Skip to content

Auth and signatures ​

The platform has three identities and each uses a different credential. Keep them separate and nothing gets confused later.

ScenarioCredentialWhere it goes
Open API (your server → platform)API KEYRequest header X-Pay-Api-Key
Merchant console (your staff → console)JWT from username/passwordRequest header x-token
Callback (platform → your server)Callback signatureRequest header X-Pay-Signature

Open API: API KEY authentication ​

Endpoints under /api/pay/open/* accept exactly one header for authentication:

http
POST {API_BASE}/api/pay/open/createOrder
X-Pay-Api-Key: <your API KEY>
Content-Type: application/json

Hard 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 401 and 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:

http
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:

http
GET {API_BASE}/api/pay/getMyProfile
x-token: <the JWT from the login response>

Common merchant-side endpoints:

EndpointPurpose
GET /api/pay/getMyProfileReturns merchantNo, collectName, a masked apiKey and cashierBaseUrl
GET /api/pay/getMyChainsThe chains and tokens you are authorized for
POST /api/pay/saveMyAddressAdd or edit a collection address (chainId / address / label)
POST /api/pay/setActiveAddressMark 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)
  • message is a string concatenation: the X-Pay-Timestamp header 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-Signature header.

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's crypto.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.

Web3 payment infrastructure for global merchants