Auth and signatures
The platform has three identities and each uses a different credential. Keep them separate and nothing gets confused later.
| Scenario | Credential | Where it goes |
|---|---|---|
| Open API (your server → platform) | API KEY | Request header X-Pay-Api-Key |
| Merchant console (your staff → console) | JWT from username/password | Request header x-token |
| Callback (platform → your server) | Callback signature | Request header X-Pay-Signature |
Open API: API KEY authentication
Endpoints under /api/pay/open/* accept exactly one header for authentication:
POST {API_BASE}/api/pay/open/createOrder
X-Pay-Api-Key: <your API KEY>
Content-Type: application/jsonHard rules about this KEY:
- It is also the callback signing key. Anyone who has it can forge a "your order was paid" callback. So it goes on your server only — never in a browser, an app bundle or a repository.
- Auth failures do not distinguish the cause. A missing KEY, a wrong KEY, a merchant that does not exist and an old KEY past its rotation window all return the same
401and the same message. That is deliberate: distinguishing them would tell a prober "this KEY exists". - Merchant identity comes only from the KEY. If the request carries a
merchantId-style field, there is no code path that can read it. - We never log the KEY in plaintext, only the last few characters for identification.
Middleware order
Open API calls pass through three middleware stages, and the order cannot be changed:
(1) per-IP fallback limit → (2) API KEY auth → (3) per-merchant limit- (1) runs before authentication, when there is no merchant identity yet, so only the source IP can stop a flood of "scripts hammering the KEY".
- (2) makes the merchant available in the request context.
- (3) limits per merchant. One merchant can rotate IPs, which makes the IP gate ineffective — and the party that suffers is its own order table.
Order creation is a low-frequency write, while queries are the main means of reconciliation (polling to cover a missed callback), so the query allowance is wider than the create allowance. The exact tiers are configured by the platform; when you trip a limit you get 429.
CORS
The Open API allows cross-origin requests: responses carry Access-Control-Allow-Origin: *, and the allowed headers include X-Pay-Api-Key.
That is safe because it does not use cookies — the credential is an explicit header value, which browsers never attach automatically. A third-party site cannot obtain the KEY, so it cannot construct an authenticated request. Correspondingly, we do not send Access-Control-Allow-Credentials.
The KEY still belongs on your server
"Cross-origin is allowed" does not mean "you may put the KEY in a browser". CORS exists for non-browser callers such as your server, and to let a frontend component embedded in your site reach your own backend. Put the KEY in frontend code and any visitor can read it straight out of the source.
Merchant console: log in for a JWT
The merchant console and merchant-side endpoints use a login session:
POST {API_BASE}/api/base/login
Content-Type: application/json
{
"username": "your-account",
"password": "your-password"
}A successful login returns a JWT valid for 7 days. Send it in the x-token header on merchant-side requests:
GET {API_BASE}/api/pay/getMyProfile
x-token: <the JWT from the login response>Common merchant-side endpoints:
| Endpoint | Purpose |
|---|---|
GET /api/pay/getMyProfile | Returns merchantNo, collectName, a masked apiKey and cashierBaseUrl |
GET /api/pay/getMyChains | The chains and tokens you are authorized for |
POST /api/pay/saveMyAddress | Add or edit a collection address (chainId / address / label) |
POST /api/pay/setActiveAddress | Mark an address active |
Get merchantNo and cashierBaseUrl from getMyProfile
The payment counter URL has the shape {cashierBaseUrl}/pay/cashier/m/{merchantNo}. cashierBaseUrl is a platform-level parameter (the checkout page is the buyer-facing front door, so the domain is the platform's choice). Read it back from getMyProfile — do not hard-code a domain.
Not yet available
Passwordless access to the merchant console is not live yet. The console still requires a username and password; do not use any entry point that claims to take you straight in.
Callbacks: the signature algorithm
When we call your server, we sign with your API KEY using HMAC-SHA256:
signature = HMAC-SHA256(key = apiKey, message = timestamp + body)messageis a string concatenation: theX-Pay-Timestampheader value directly followed by the raw request body. Not an addition, not a field extracted from the JSON.- The result is a lowercase hex string, sent in the
X-Pay-Signatureheader.
Two requirements when verifying:
- Compare against raw bytes. Parse the body into an object and re-serialise it, and a changed field order or whitespace will break the signature.
- Use a constant-time comparison (Go's
hmac.Equal, Node'scrypto.timingSafeEqual). A short-circuiting byte-wise comparison leaks timing that can be used to guess the signature bit by bit.
Full callback handling, retries and idempotency are in Callbacks and verification.
Key rotation
You will want a new KEY if the current one leaks. After rotation there is a grace window in which both the new and the previous KEY work, so the old KEY running in production does not break the very instant we rotate — you can update your configuration at your own pace.
- Using the previous KEY during the grace window is allowed, but we log a warning.
- Once the window closes, the previous KEY is rejected.
- Update your callback verification key after a rotation — it is the same KEY.
Getting and resetting your KEY
After signing up, open the merchant dashboard under “Payments → Payment settings” and click Reveal to see your current key (we never email it; every reveal is audit-logged). To rotate it, click Reset on the same page — the old and new KEY both work during a transition window, so you can switch on your own schedule.