Skip to content

Java — API Checkout

REPLACE EXAMPLE CREDENTIALS

Replace all placeholder values before running:

  • "your-api-secret" → Your actual API Secret from the merchant portal
  • "MCH_20240101_ABC123" → Your actual Merchant ID

API Checkout does not use X-Api-Key. Only X-Timestamp and X-Signature are required.

PERMISSION REQUIRED

API Checkout must be explicitly enabled for your account. Contact support to activate it.

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

API Checkout uses the same signing algorithm as Hosted Payments but without X-Api-Key.

  • POST requests: Base64(HMAC-SHA256(timestamp\nPOST\npath\nBase64(SHA256(body)), apiSecret))
  • GET requests (no query string): Base64(HMAC-SHA256(timestamp\nGET\npath, 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 CheckoutSignatureHelper {

    private final String apiSecret;

    public CheckoutSignatureHelper(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);
    }

    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);
    }
}

Step 1: Fetch Available Payment Methods

Call this at runtime before rendering your payment UI. Never hardcode currency/network combinations.

java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class GetPaymentMethodsExample {

    private static final String BASE_URL    = "https://api.paystablecoin.global";
    private static final String MERCHANT_ID = "MCH_20240101_ABC123";
    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 + "/checkout/payment-methods";

        CheckoutSignatureHelper signer = new CheckoutSignatureHelper(API_SECRET);
        String signature = signer.signGet(timestamp, path);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(BASE_URL + path))
            .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());
        // Response includes: cryptoCurrency, network, networkDisplayName,
        // minAmount, maxAmount, estimatedConfirmationTimeSec, displayOrder
        // Sort by displayOrder (ascending) and render options to the user.
    }
}

Step 2: Create a Checkout Order

After the customer selects a currency and network, create the order immediately.

java
public class CreateCheckoutOrderExample {

    private static final String BASE_URL    = "https://api.paystablecoin.global";
    private static final String MERCHANT_ID = "MCH_20240101_ABC123";
    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 + "/checkout/orders";

        // orderAmount.currency must be a stablecoin (USDC, USDT, or USD1)
        // network must match the customer's selected payment method
        String body = String.format(
            "{\"merchantOrderId\":\"ORDER_%s\",\"orderAmount\":{\"value\":\"100.50\",\"currency\":\"USDC\"}," +
            "\"paymentMethodType\":\"ON_CHAIN_TRANSFER\",\"network\":\"tron\"," +
            "\"expiresAt\":\"2026-12-31T23:59:59Z\"," +
            "\"callbackUrl\":\"https://yoursite.com/webhook/checkout\"}",
            timestamp
        );

        CheckoutSignatureHelper signer = new CheckoutSignatureHelper(API_SECRET);
        String signature = signer.signPost(timestamp, path, body);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(BASE_URL + path))
            .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());
        // Response includes:
        //   data.depositAddress     — show as text + QR code
        //   data.cryptoPaymentAmount.value  — exact amount customer must send
        //   data.cryptoPaymentAmount.currency
        //   data.networkDisplayName — show prominently (wrong network = lost funds)
        //   data.expiresAt          — drive a countdown timer
        //
        // IMPORTANT: Display cryptoPaymentAmount, NOT orderAmount
    }
}

Step 3: Query Checkout Order Status

Use as a fallback if the webhook is not received.

java
public class QueryCheckoutOrderExample {

    private static final String BASE_URL    = "https://api.paystablecoin.global";
    private static final String MERCHANT_ID = "MCH_20240101_ABC123";
    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 + "/checkout/orders/" + merchantOrderId;

        CheckoutSignatureHelper signer = new CheckoutSignatureHelper(API_SECRET);
        String signature = signer.signGet(timestamp, path);

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(BASE_URL + path))
            .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());
        // Poll every 5–10 seconds until status is terminal:
        // SUCCEEDED, FAILED, or CLOSED
    }
}

Step 4: Verify Incoming Webhook

API Checkout webhooks require a strict response: HTTP 200 with {"code":"00000"} within 5 seconds.

java
import com.sun.net.httpserver.HttpExchange;

public class CheckoutWebhookHandler {

    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
        CheckoutSignatureHelper signer = new CheckoutSignatureHelper(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 + {"code":"00000"} within 5 seconds (REQUIRED)
        byte[] responseBody = "{\"code\":\"00000\"}".getBytes(StandardCharsets.UTF_8);
        exchange.getResponseHeaders().set("Content-Type", "application/json");
        exchange.sendResponseHeaders(200, responseBody.length);
        exchange.getResponseBody().write(responseBody);
        exchange.close();

        // Step 4: Process business logic asynchronously
        // Use acquiringOrderId or merchantOrderId for deduplication
        // The same event may be delivered more than once.
    }
}

Released under the MIT License.