Skip to main content

Standard Payment Protocol

Audience: Third-party payment service providers (PSPs)

Protocol version: STANDARD_V1

Signature algorithm: HMAC_SHA256

Integration mode: REDIRECT (hosted checkout page)

Document date: 2026-07-20 (revised 2026-07-29: amount unit corrected to major currency units in both directions; /return endpoint semantics clarified)

This document is the complete customer-facing integration guide for connecting your payment service to the merchant platform via Standard Payment Protocol v1.

It covers:

  1. The HTTP API you must expose (POST /v1/payments/init)
  2. The asynchronous payment notify you must send to the platform
  3. The shared HMAC-SHA256 signature scheme used in both directions
  4. Amount / currency rules, security checklist, and FAQ

Before you start​

This guide is the technical reference for PSP developers. If you are a platform administrator looking to configure a payment provider in the Admin Console, see Payment Providers for the UI workflow.


Overview​

RoleResponsibility
Merchant platformCreates payment orders, redirects users to your checkout page, receives payment result notifications, credits the user
Your PSPExposes POST /v1/payments/init, hosts the checkout UI, collects payment, sends a signed async notify back to the platform

End-to-end flow​

User                Merchant Platform                       Your PSP
| | |
|-- select channel ------>| |
| |-- POST /v1/payments/init (signed)->|
| |<-- { paymentId, checkoutUrl } -----|
|<-- redirect checkoutUrl-| |
|---------------------- pay on your page --------------------->|
| |<-- POST .../notify (signed) -------|
| |-- HTTP 200 {"code":0,"message":"ok"}
| | (credit user on SUCCESS) |

Environments & base URLs​

ItemDescription
Your API base URLConfigured per tenant as apiBaseUrl (no trailing slash), e.g. https://psp.example.com
Platform callback base URLProvided during onboarding / tenant config (callbackBaseUrl), e.g. https://console.example.com
Notify URL template{callbackBaseUrl}/xt-console/api/payment/callback/{providerCode}/{partnerId}/notify

providerCode and partnerId are assigned during onboarding. Treat them as opaque path segments.


Credentials and onboarding​

Each tenant (partner) is configured with:

FieldRequiredDescription
merchantIdYesMerchant identifier issued by you
apiBaseUrlYesBase URL of your Standard Payment API
secretKeyYesShared secret for HMAC-SHA256 (UTF-8)
defaultCurrencyNoDefault ISO-4217 currency, e.g. USD
paymentLifetimeSecondsNoOrder lifetime in seconds (platform default: 900)
ipAllowlistNoIf set, only these source IPs may call notify

Reserved provider codes that cannot be used for Standard v1: stripe, freedompay.

After configuration, the tenant must test → activate the channel before checkout is allowed. (The test step currently validates configuration completeness — merchantId, apiBaseUrl, secretKey — it does not call your API. Verify end-to-end connectivity with a sandbox round-trip, see §9.)


API you must implement​

Init payment​

Create a payment session and return a hosted checkout URL.

Endpoint

POST {apiBaseUrl}/v1/payments/init
Content-Type: application/json
X-Pay-Timestamp: {unix_seconds}
X-Pay-Sign: {hmac_hex}

Request body

FieldTypeRequiredDescription
merchantIdstringYesMerchant ID from tenant config
orderIdstringYesPlatform order ID (opaque). Format example: SP{millis}{8hex}
amountstringYesAmount in major currency units as a decimal string (see §5). Integral amounts are sent without decimal places (e.g. "10", not "10.00"); non-integral amounts use two decimals (e.g. "10.50"). Always use the received raw string when verifying the signature
currencystringYesISO-4217 currency code, uppercase (e.g. USD)
notifyUrlstringYesAbsolute URL for async payment result notification
returnUrlstringYesBrowser return URL after payment (may include transactionNo)
timestampstringYesUnix epoch seconds (same value as X-Pay-Timestamp)

Example:

{
"merchantId": "M0001",
"orderId": "SP1234567890abcdef",
"amount": "10",
"currency": "USD",
"notifyUrl": "https://console.example.com/xt-console/api/payment/callback/acme_pay/42/notify",
"returnUrl": "https://merchant.example.com/billing/success?transactionNo=SP1234567890abcdef",
"timestamp": "1751450400"
}

Note: The signature is sent in the X-Pay-Sign header, not in the JSON body.

Success response (HTTP 2xx)

{
"code": 0,
"message": "ok",
"data": {
"paymentId": "pay_001",
"checkoutUrl": "https://psp.example.com/checkout/pay_001",
"expireAt": "2026-07-20T08:00:00Z"
}
}
FieldRequiredDescription
codeYesMust be 0 for success
data.paymentIdYesYour payment ID (stored as external reference)
data.checkoutUrlYesAbsolute URL to redirect the payer
data.expireAtRecommendedSession / payment expiry (ISO-8601 or equivalent string)

Failure response

Return a non-zero code and/or a non-2xx HTTP status. The platform will abort checkout.

Idempotency (recommended)

If the same orderId is initialized again, return the existing paymentId / checkoutUrl instead of creating a duplicate order.


Async notification​

After the payer completes (or fails / abandons) payment, call the notifyUrl from the init request.

Endpoint​

POST {notifyUrl}
Content-Type: application/json
X-Pay-Timestamp: {unix_seconds}
X-Pay-Sign: {hmac_hex}

GET with query parameters is also accepted. Prefer POST + JSON body.

There is also an alternative GET-form notification endpoint:

GET {callbackBaseUrl}/xt-console/api/payment/callback/{providerCode}/{partnerId}/return

It uses the same verification & processing pipeline as /notify and responds with a JSON ack (not a redirect). Do not use it as a browser redirect target — the payer would see raw JSON. For the browser return after payment, redirect the payer to the returnUrl provided in the init request (e.g. via a "Back to merchant" button or auto-redirect on your checkout result page). Always use server-to-server /notify as the source of truth.

Notify fields​

FieldRequiredDescription
orderIdYesMust equal the init orderId
statusYesSUCCESS / FAILED / CLOSED (case-insensitive)
timestampYesUnix epoch seconds
signYes*Signature (optional if X-Pay-Sign header is present)
amountRequired for SUCCESSMajor currency units, same as init. Must be numerically equal to the platform order amount (see §5)
currencyRequired for SUCCESSMust match the platform order currency (case-insensitive)
paymentIdRecommendedYour payment ID

* Provide signature via X-Pay-Sign header and/or body/query field sign.

Example body (SUCCESS):

{
"orderId": "SP1234567890abcdef",
"status": "SUCCESS",
"amount": "10.00",
"currency": "USD",
"paymentId": "pay_001",
"timestamp": "1751450400",
"sign": "<hmac_hex>"
}

Status semantics​

statusPlatform behavior
SUCCESSMark order paid and credit the user (idempotent if already success)
FAILEDMark order failed
CLOSEDMark order closed / cancelled
otherRejected with HTTP 400

Ack responses​

HTTPBodyMeaning
200{"code":0,"message":"ok"}Accepted — stop retrying
400{"code":400,"message":"orderId is required"}Bad request
400{"code":400,"message":"amount mismatch"}SUCCESS amount/currency mismatch
400{"code":400,"message":"invalid order status"}Order not in a payable state
400{"code":400,"message":"unsupported status"}Unknown status
401{"code":401,"message":"invalid signature"}Signature verification failed
401{"code":401,"message":"timestamp expired"}Timestamp outside allowed skew
403{"code":403,"message":"ip not allowed"}Source IP not in allowlist
404{"code":404,"message":"order not found"}Unknown orderId

Retry and idempotency​

  • Retry notify until you receive HTTP 200 with code:0.
  • Use exponential backoff (e.g. 1s, 5s, 30s, 2m, …).
  • Duplicate SUCCESS notifies for an already-successful order also return 200 / ok (safe to retry).
  • Timestamp skew window: ±300 seconds by default.

Amount and currency rules​

DirectionUnitExample (USD 10.00)
Init request (platform → PSP)Major currency decimal string"10" (integral) or "10.50"
SUCCESS notify (PSP → platform)Major currency decimal string (same unit as init)"10", "10.0" or "10.00"
CurrencyISO-4217"USD"

Rules:

  1. Always use a string amount (avoid floating-point JSON numbers).
  2. Currency must be a 3-letter ISO code (uppercase recommended).
  3. Init sends the amount in major currency units. Integral amounts have no decimal places ("10"); non-integral amounts use two decimals ("10.50").
  4. On SUCCESS notify, amount is also in major currency units and must be numerically equal to the platform order amount (tolerance < 0.001). Formatting variants such as "10", "10.0" and "10.00" are all accepted. Example: init sent "10" → notify with "10" or "10.00". Never send minor units (cents) — e.g. "1000" for USD 10 will be rejected with amount mismatch.
  5. On SUCCESS notify, currency must match the order currency (case-insensitive).
  6. For FAILED / CLOSED, amount/currency are not validated the same way, but including them is still recommended for reconciliation.

Signature algorithm​

Both directions use the same algorithm.

Steps​

  1. Collect all business parameters as a Map<String, String>.
  2. Remove the key sign (case-insensitive) if present.
  3. Drop entries whose value is blank / null.
  4. Sort remaining entries by key ascending (ASCII / TreeMap order).
  5. Join as key=value&key=value&... (no URL-encoding of values for the sign payload).
  6. Build the payload string (four lines, \n separators):
{HTTP_METHOD_UPPERCASE}
{path}
{timestamp}
{sorted_params}
  1. Compute HMAC-SHA256(payload, secretKey) with UTF-8 encoding.
  2. Output lowercase hex (%02x per byte).
  3. Verification is case-insensitive.

Path rules​

CallMethodpath used in signature
Init payment (platform → you)POST/v1/payments/init
Notify (you → platform)POST or GETFull request URI path, e.g. /xt-console/api/payment/callback/acme_pay/42/notify

For notify, sign the path only (no scheme/host/query). Example: /xt-console/api/payment/callback/acme_pay/42/notify.

Headers​

HeaderUsage
X-Pay-TimestampUnix seconds. For notify, signature and skew validation are based on the body/query timestamp field; send the same value in this header for transport redundancy
X-Pay-SignHex HMAC signature

Worked example: init​

Secret: your-shared-secret Timestamp: 1751450400

Sorted params:

amount=10&currency=USD&merchantId=M0001&notifyUrl=https://console.example.com/xt-console/api/payment/callback/acme_pay/42/notify&orderId=SP1234567890abcdef&returnUrl=https://merchant.example.com/billing/success?transactionNo=SP1234567890abcdef&timestamp=1751450400

Payload:

POST
/v1/payments/init
1751450400
amount=10&currency=USD&merchantId=M0001&notifyUrl=https://console.example.com/xt-console/api/payment/callback/acme_pay/42/notify&orderId=SP1234567890abcdef&returnUrl=https://merchant.example.com/billing/success?transactionNo=SP1234567890abcdef&timestamp=1751450400

Signature (HMAC-SHA256 hex):

9ad342530a3834774168e93d0a46c0fe7de6e3124b99006b662823db9f8ac347

Pseudo-code (Java):

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secretKey.getBytes(UTF_8), "HmacSHA256"));
byte[] hash = mac.doFinal(payload.getBytes(UTF_8));
// format each byte as lowercase hex → X-Pay-Sign

Pseudo-code (Python):

import hmac, hashlib
sign = hmac.new(secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256).hexdigest()

Worked example: SUCCESS notify​

Method: POST Path: /xt-console/api/payment/callback/acme_pay/42/notify Secret: your-shared-secret Timestamp: 1751450400

Sorted params (amount in major currency units, same as init):

amount=10.00&currency=USD&orderId=SP1234567890abcdef&paymentId=pay_001&status=SUCCESS&timestamp=1751450400

Payload:

POST
/xt-console/api/payment/callback/acme_pay/42/notify
1751450400
amount=10.00&currency=USD&orderId=SP1234567890abcdef&paymentId=pay_001&status=SUCCESS&timestamp=1751450400

Signature (HMAC-SHA256 hex):

3944d5892c8781e35d2bd2bc72a9b065d5c26219e9eb38e504de7d3e675ecddd

Security checklist​

  • Keep secretKey only on the server; never expose it to browsers or mobile apps.
  • Reject init / notify requests with invalid signatures.
  • Reject timestamps outside the allowed skew (±300s).
  • Prefer HTTPS for both your API and the platform notify URL.
  • Optionally configure an IP allowlist for notify sources.
  • Treat orderId as opaque; do not parse platform-specific formats.
  • On SUCCESS, send amount in major currency units (numerically equal to the platform order amount) and matching currency.

These endpoints are provided by the merchant platform (not implemented by the PSP). Listed here so you understand how traffic reaches you.

MethodPathAuthPurpose
GET/xt-console/api/standard-payment/channelsLoginList active payment channels for the partner
POST/xt-console/api/standard-payment/checkoutLoginCreate checkout → calls your /v1/payments/init
POST/GET/xt-console/api/payment/callback/{providerCode}/{partnerId}/notifyPublicReceives your payment notify
GET/xt-console/api/payment/callback/{providerCode}/{partnerId}/returnPublicGET-form notify alternative (same processing as notify, JSON response — not a browser redirect)

Admin configuration (login required): /xt-console/api/admin/payment-providers/**


Integration checklist for PSPs​

  1. Implement POST /v1/payments/init with signature verification.
  2. Host a checkout page at the returned checkoutUrl.
  3. After payment, POST a signed notify to notifyUrl.
  4. On SUCCESS, send amount in major currency units (numerically equal to the platform order) and matching currency.
  5. Retry until HTTP 200 + {"code":0,"message":"ok"}.
  6. Provide sandbox credentials (merchantId, apiBaseUrl, secretKey) for tenant onboarding.
  7. Confirm notify source IPs if allowlisting is required.
  8. Validate a full round-trip in the sandbox (init → pay → SUCCESS notify → credit).

FAQ​

Q: Can I put the signature in the JSON body? A: For init, the platform sends X-Pay-Sign in the header (body has no sign). For notify, you may send either the header or a sign field (or both).

Q: What if my notify arrives twice? A: Safe. A second SUCCESS for an already-successful order returns 200 / ok.

Q: Which unit does amount use? A: Major currency units in both directions (init and notify), as a decimal string. The notify amount is compared numerically against the order amount, so "10", "10.0" and "10.00" are equivalent. Do not convert to cents — sending "1000" for a USD 10 order will be rejected with amount mismatch.

Q: Do I need to implement refund / query APIs? A: Not required by Standard Payment v1 today. Capabilities may be extended later.

Q: Which character encoding? A: UTF-8 for both the payload string and the secret key bytes.


Contact and change control​

  • Protocol adapter type: STANDARD_V1
  • Signature algorithm label: HMAC_SHA256
  • Breaking changes to path layout, required fields, or the signature payload format will bump the protocol version.

For onboarding, sandbox credentials, or production cutover, contact your integration owner.