Java — Hosted Payments
REPLACE EXAMPLE CREDENTIALS
Replace all placeholder values before running:
"your-api-key"→ Your actual API Key from the merchant portal"your-api-secret"→ Your actual API Secret from the merchant portal"1000"→ Your actual Merchant ID
Dependencies (Maven)
xml
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>Signature Helper
All Hosted Payments requests require X-Api-Key, X-Timestamp, and X-Signature.
- POST requests:
Base64(HMAC-SHA256(timestamp\nPOST\npath\nBase64(SHA256(body)), apiSecret)) - GET requests (no query string):
Base64(HMAC-SHA256(timestamp\nGET\npath, apiSecret)) - GET requests (with query string):
Base64(HMAC-SHA256(timestamp\nGET\npath\nBase64(SHA256(queryString)), apiSecret))
java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
public class SignatureHelper {
private final String apiSecret;
public SignatureHelper(String apiSecret) {
this.apiSecret = apiSecret;
}
/** Sign a POST request (body present). */
public String signPost(String timestamp, String path, String body) throws Exception {
byte[] bodyHash = MessageDigest.getInstance("SHA-256")
.digest(body.getBytes(StandardCharsets.UTF_8));
String bodyHashB64 = Base64.getEncoder().encodeToString(bodyHash);
return hmacBase64(timestamp + "\n" + "POST" + "\n" + path + "\n" + bodyHashB64);
}
/** Sign a GET request with no query string. */
public String signGet(String timestamp, String path) throws Exception {
return hmacBase64(timestamp + "\n" + "GET" + "\n" + path);
}
/** Sign a GET request that has a query string (e.g. refund query). */
public String signGetWithQuery(String timestamp, String path, String queryString) throws Exception {
byte[] qsHash = MessageDigest.getInstance("SHA-256")
.digest(queryString.getBytes(StandardCharsets.UTF_8));
String qsHashB64 = Base64.getEncoder().encodeToString(qsHash);
return hmacBase64(timestamp + "\n" + "GET" + "\n" + path + "\n" + qsHashB64);
}
private String hmacBase64(String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
/** Verify an incoming webhook signature. */
public boolean verifyWebhook(String timestamp, String signature, String rawBody) throws Exception {
String expected = hmacBase64(timestamp + "\n" + rawBody);
return expected.equals(signature);
}
}Create Payment Order
java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class CreateOrderExample {
private static final String BASE_URL = "https://api.paystablecoin.global";
private static final String MERCHANT_ID = "1000";
private static final String API_KEY = "your-api-key";
private static final String API_SECRET = "your-api-secret";
public static void main(String[] args) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis());
String path = "/api/v1/merchants/" + MERCHANT_ID + "/orders";
// Build request body (must be minified — no extra whitespace)
String body = String.format(
"{\"merchantOrderId\":\"ORDER_%s\",\"orderAmount\":{\"value\":\"99.99\",\"currency\":\"USD\"}," +
"\"expiresAt\":\"2026-12-31T23:59:59Z\"," +
"\"returnUrl\":\"https://yoursite.com/payment/success\"," +
"\"callbackUrl\":\"https://yoursite.com/webhook/payment\"}",
timestamp
);
SignatureHelper signer = new SignatureHelper(API_SECRET);
String signature = signer.signPost(timestamp, path, body);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + path))
.header("X-Api-Key", API_KEY)
.header("X-Timestamp", timestamp)
.header("X-Signature", signature)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
// Expected: {"code":"00000","data":{"status":"INIT","checkoutUrl":"https://..."}}
// Redirect the customer to checkoutUrl
}
}Query Order
java
public class QueryOrderExample {
private static final String BASE_URL = "https://api.paystablecoin.global";
private static final String MERCHANT_ID = "1000";
private static final String API_KEY = "your-api-key";
private static final String API_SECRET = "your-api-secret";
public static void main(String[] args) throws Exception {
String merchantOrderId = "ORDER_1738112345000"; // Replace with your order ID
String timestamp = String.valueOf(System.currentTimeMillis());
String path = "/api/v1/merchants/" + MERCHANT_ID + "/orders/" + merchantOrderId;
SignatureHelper signer = new SignatureHelper(API_SECRET);
String signature = signer.signGet(timestamp, path);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + path))
.header("X-Api-Key", API_KEY)
.header("X-Timestamp", timestamp)
.header("X-Signature", signature)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
// Check response "data.status": INIT / PROCESSING / SUCCEEDED / FAILED / CLOSED
}
}Verify Incoming Webhook
java
import com.sun.net.httpserver.HttpExchange;
public class WebhookHandler {
private static final String API_SECRET = "your-api-secret";
public void handle(HttpExchange exchange) throws Exception {
String timestamp = exchange.getRequestHeaders().getFirst("X-Timestamp");
String signature = exchange.getRequestHeaders().getFirst("X-Signature");
String rawBody = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
// Step 1: Verify signature before processing
SignatureHelper signer = new SignatureHelper(API_SECRET);
if (!signer.verifyWebhook(timestamp, signature, rawBody)) {
exchange.sendResponseHeaders(401, 0);
exchange.close();
return;
}
// Step 2: Verify timestamp is within 5 minutes
long webhookTime = Long.parseLong(timestamp);
if (Math.abs(System.currentTimeMillis() - webhookTime) > 300_000) {
exchange.sendResponseHeaders(401, 0);
exchange.close();
return;
}
// Step 3: Return HTTP 200 immediately, process business logic asynchronously
exchange.sendResponseHeaders(200, 0);
exchange.close();
// Step 4: Parse and handle the event (asynchronously)
// ObjectMapper mapper = new ObjectMapper();
// Map<String, Object> event = mapper.readValue(rawBody, Map.class);
// String status = (String) event.get("status");
// Handle: PROCESSING, SUCCEEDED
// Use acquiringOrderId or merchantOrderId for deduplication
}
}Create Refund
java
public class CreateRefundExample {
private static final String BASE_URL = "https://api.paystablecoin.global";
private static final String MERCHANT_ID = "1000";
private static final String API_KEY = "your-api-key";
private static final String API_SECRET = "your-api-secret";
public static void main(String[] args) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis());
String path = "/api/v1/merchants/" + MERCHANT_ID + "/refunds";
String body = "{\"merchantRefundOrderId\":\"REFUND_" + timestamp + "\"," +
"\"acquiringOrderId\":\"ORDER_1738112345000\"," +
"\"refundReason\":\"Customer request full refund\"}";
SignatureHelper signer = new SignatureHelper(API_SECRET);
String signature = signer.signPost(timestamp, path, body);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + path))
.header("X-Api-Key", API_KEY)
.header("X-Timestamp", timestamp)
.header("X-Signature", signature)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
// On success: {"code":"00000","data":{"refundOrderId":"REF_...","status":"PROCESSING",...}}
}
}Query Refund
java
public class QueryRefundExample {
private static final String BASE_URL = "https://api.paystablecoin.global";
private static final String MERCHANT_ID = "1000";
private static final String API_KEY = "your-api-key";
private static final String API_SECRET = "your-api-secret";
public static void main(String[] args) throws Exception {
String merchantRefundOrderId = "REFUND_1738112345000"; // Replace with your refund order ID
String timestamp = String.valueOf(System.currentTimeMillis());
String path = "/api/v1/merchants/" + MERCHANT_ID + "/refunds";
String queryString = "merchantRefundOrderId=" + merchantRefundOrderId;
SignatureHelper signer = new SignatureHelper(API_SECRET);
String signature = signer.signGetWithQuery(timestamp, path, queryString);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(BASE_URL + path + "?" + queryString))
.header("X-Api-Key", API_KEY)
.header("X-Timestamp", timestamp)
.header("X-Signature", signature)
.GET()
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
// Check data.status: PROCESSING / SUCCEEDED / FAILED / CLOSED
}
}Related Documentation
- Hosted Payments Integration Guide — Step-by-step integration walkthrough
- API Reference — Full endpoint and field specification
- Signature Algorithm — Signature calculation details