Official Java client library for the SurePay Merchant API.
- Java 17+
- Jackson Databind (only runtime dependency)
Maven
Add the GitHub Packages repository and dependency to your pom.xml:
<repositories>
<repository>
<id>github</id>
<url>https://maven.pkg.github.com/surepay-one/surepay-java-sdk</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>one.surepay</groupId>
<artifactId>surepay-java-sdk</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>Gradle
repositories {
maven {
url = uri("https://maven.pkg.github.com/surepay-one/surepay-java-sdk")
credentials {
username = project.findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")
password = project.findProperty("gpr.token") ?: System.getenv("GITHUB_TOKEN")
}
}
}
dependencies {
implementation 'one.surepay:surepay-java-sdk:0.1.0'
}GitHub Packages requires authentication even for public packages. Create a personal access token with
read:packagesscope.
import one.surepay.sdk.SurePay;
import one.surepay.sdk.model.*;
SurePay client = SurePay.builder(
System.getenv("SUREPAY_API_KEY"), // tpay_live_... or tpay_test_...
System.getenv("SUREPAY_API_SECRET") // tpay_sec_... — enables auto HMAC signing
).build();
// Check wallet balance
Balance bal = client.balance.get();
System.out.printf("Available: %,d %s%n", bal.available(), bal.currency());
// Create a deposit order (thu hộ)
Deposit dep = client.deposits.create(
CreateDepositRequest.builder(100_000L)
.requestId("ORD-20260610-001")
.build()
);
System.out.println("Checkout URL: " + dep.checkoutUrl());
// Create a payout (chi hộ)
try {
Payout pay = client.payouts.create(
CreatePayoutRequest.builder(500_000L, "VCB", "1234567890", "NGUYEN VAN A", "Salary June 2026")
.build()
);
System.out.println("Payout ID: " + pay.id());
} catch (SurePayException e) {
if (e.isInsufficientBalance()) {
System.out.println("Not enough balance — top up first");
}
}SurePay client = SurePay.builder(apiKey, apiSecret)
.baseUrl("https://api.surepay.one/merchant/v1") // override for local/staging
.timeout(Duration.ofSeconds(15))
.maxRetries(3) // retries on 5xx and network errors
.build();| Option | Default | Description |
|---|---|---|
baseUrl(String) |
https://api.surepay.one/merchant/v1 |
Override base URL for local dev or staging |
timeout(Duration) |
30s |
HTTP request timeout |
maxRetries(int) |
3 |
Retry attempts on 5xx and network errors |
Every request requires an API key sent as an X-API-Key header. When apiSecret is provided to builder(), every outgoing request is automatically signed with HMAC-SHA256 — the X-Signature and X-Timestamp headers are attached with no extra code needed.
Get current wallet balance. Requires: balance:read scope.
Balance bal = client.balance.get();
// bal.balance() — total wallet balance in VND
// bal.hold() — reserved for in-flight transactions
// bal.available() — bal.balance() - bal.hold()
// bal.currency() — always "VND"Paginated list of deposit (thu hộ) orders. Requires: deposits:read scope.
ListResult<Deposit> result = client.deposits.list(
ListDepositsParams.create()
.page(1)
.pageSize(20)
.status("success") // pending|processing|success|failed|expired|cancelled
.search("ORD-2026")
.fromDate("2026-06-01") // YYYY-MM-DD
.toDate("2026-06-30")
);
// result.items() — List<Deposit>
// result.total() — total matching records
// result.totalPages() — total pagesCreate a new deposit order. Returns a checkoutUrl (redirect) and qrCode (VietQR). Requires: deposits:write scope.
Deposit dep = client.deposits.create(
CreateDepositRequest.builder(100_000L) // amount in VND, required
.requestId("ORD-20260610-001") // your order ID — optional, for idempotency
.currency("VND") // optional, default "VND"
// Chính chủ verification — all optional:
.senderBankId("970436")
.senderBankName("Vietcombank")
.senderAccount("1234567890")
.senderName("NGUYEN VAN A")
.build()
);
System.out.println(dep.checkoutUrl());
System.out.println(dep.qrCode());Response fields:
| Field | Type | Description |
|---|---|---|
id() |
String | SurePay transaction UUID |
requestId() |
String | Your order ID |
amount() |
long | Amount in VND |
status() |
DepositStatus | pending, processing, success, failed, expired, cancelled |
fee() |
long | Transaction fee in VND |
checkoutUrl() |
String | Redirect URL for payer |
qrCode() |
String | VietQR data string |
accountNumber() |
String | Receiving bank account number |
accountName() |
String | Receiving account holder name |
bankBin() |
String | Receiving bank BIN |
expiresAt() |
Instant | Order expiry time |
isOwnerVerified() |
Boolean | Chính chủ result — null until transfer received |
Fetch a single deposit order by UUID. Requires: deposits:read scope.
Deposit dep = client.deposits.get("uuid-here");
// dep.status() == DepositStatus.PENDING | DepositStatus.SUCCESS | ...Paginated list of payout (chi hộ) orders. Requires: payouts:read scope.
ListResult<Payout> result = client.payouts.list(
ListPayoutsParams.create()
.page(1)
.pageSize(20)
.status("success") // pending|processing|success|failed
.fromDate("2026-06-01")
.toDate("2026-06-30")
);Initiate a payout bank transfer. Funds are deducted from your wallet immediately on success. Requires: payouts:write scope.
Payouts are irreversible once status moves past
pending. Verify bank details withclient.bankInquiry.verify()first.
Payout pay = client.payouts.create(
CreatePayoutRequest.builder(
500_000L, // amount in VND, required
"VCB", // bank code, required (VCB, MB, TCB, ACB, ...)
"1234567890", // bank account, required
"NGUYEN VAN A", // full name in UPPERCASE, required
"Salary June" // description / transfer memo, required
)
.bankName("Vietcombank") // optional
.build()
);Fetch a single payout by UUID. Requires: payouts:read scope.
Payout pay = client.payouts.get("uuid-here");
// pay.status() == PayoutStatus.PENDING | PayoutStatus.SUCCESS | ...Look up the account holder name for a bank account. Call this before creating a payout to confirm the recipient. Requires: payouts:read scope.
BankInquiryResult info = client.bankInquiry.verify(
BankInquiryRequest.of("VCB", "1234567890")
);
System.out.println("Account name: " + info.accountName());Pass an idempotency key as a second argument to any create() method. The key is forwarded as an Idempotency-Key header — safe to retry on network errors without risk of duplicate transactions.
Deposit dep = client.deposits.create(
CreateDepositRequest.builder(100_000L).requestId("ORD-20260610-001").build(),
"ORD-20260610-001" // idempotency key
);Every inbound webhook event from SurePay is HMAC-signed. Pass the raw request body bytes (before any JSON parsing) to client.webhooks.verify():
// Spring Boot example
@PostMapping("/webhook/surepay")
public ResponseEntity<Void> handleWebhook(HttpServletRequest request) throws IOException {
byte[] body = request.getInputStream().readAllBytes();
if (!client.webhooks.verify(body)) {
return ResponseEntity.status(401).build();
}
// Parse event type
Map<String, Object> event = new ObjectMapper().readValue(body, Map.class);
String eventType = (String) event.get("event");
switch (eventType) {
case "deposit.success", "deposit.failed" -> {
// handle deposit event
}
case "payout.success", "payout.failed" -> {
// handle payout event
}
}
return ResponseEntity.ok().build();
}Or pass the X-Surepay-Signature header value explicitly:
String sig = request.getHeader("X-Surepay-Signature");
boolean valid = client.webhooks.verify(body, sig);try {
Payout pay = client.payouts.create(req);
} catch (SurePayException e) {
// Convenience helpers
if (e.isNotFound()) { /* 404 not_found */ }
if (e.isRateLimit()) { /* 429 rate_limit_exceeded */ }
if (e.isInsufficientBalance()) { /* 422 insufficient_balance */ }
if (e.isDuplicate()) { /* 409 duplicate_request */ }
// Full details
System.out.printf("HTTP %d code=%s %s%n",
e.getStatusCode(), e.getCode(), e.getMessage());
}Error codes:
| HTTP | getCode() |
Meaning |
|---|---|---|
| 400 | validation_error |
Invalid request body or parameters |
| 401 | unauthorized |
Missing or invalid API key |
| 401 | signature_invalid |
HMAC signature failed or timestamp > 5 min |
| 403 | permission_denied |
API key lacks required scope |
| 403 | ip_not_allowed |
Request IP not in allowlist |
| 404 | not_found |
Resource not found |
| 409 | duplicate_request |
Idempotency key conflict |
| 422 | insufficient_balance |
Top up wallet first |
| 422 | invalid_state_transition |
Operation not allowed for current status |
| 429 | rate_limit_exceeded |
Slow down — back off and retry |
| 500 | internal_error |
Server error |
Use ErrorCode.* constants to compare codes without hard-coding strings:
if (ErrorCode.INSUFFICIENT_BALANCE.equals(e.getCode())) { ... }When apiSecret is set, all requests are signed automatically. The signing algorithm for manual use:
signingString = UNIX_TIMESTAMP + "\n" + METHOD + "\n" + PATH + "\n" + hex(sha256(body))
signature = "sha256=" + hex(hmac-sha256(apiSecret, signingString))
Attach as headers: X-Signature: <signature> and X-Timestamp: <unix_timestamp>.
Signatures expire after 300 seconds — generate per-request, never cache or reuse.
MIT