Webhooks
Payment Notification
When an order reaches its final status, AinePay pushes a notification of the order status change to the merchant's system so the merchant can process it in a timely manner.
Endpoint
- Method:
POST - Target URL:
{notifyUrl}/ainepay/notify - Authentication: Signature verification with timestamp window check
- Content-Type:
application/x-www-form-urlencoded - Headers:
x-api-signature: lowercase hex HMAC-SHA256x-api-timestamp: millisecond timestamp, included in the signaturex-api-recv-window: accept window in milliseconds, included in the signature
Delivery Behavior
- A webhook is created when the order status changes in a way that needs to notify the merchant.
- In the current implementation, AinePay sends notifications for
PAID,EXPIRED,CANCEL, andREFUND. - Orders created through a payment link (PAYMENT_LINK) do not trigger callbacks.
- AinePay treats any HTTP
2xxresponse as a successful acknowledgement (redirects are not followed). - If the endpoint returns a non-
2xxstatus or the request fails, AinePay retries with exponential backoff: base delay5s, the nth retry waits5s × 2^(n-1), up to5attempts, after which it stops retrying. - When the merchant is rate-limited, the delivery is rescheduled with a
30sbackoff.
Request Fields
| field | type | description | required | example |
|---|---|---|---|---|
| merchantId | string | Merchant ID. | yes | 20001 |
| orderId | string | Merchant order ID. | yes | ORDER_10001 |
| userId | string | Merchant-defined user ID. | yes | U_90001 |
| coin | string | Token symbol. | yes | USDT |
| chain | string | Chain code. | yes | ETH |
| qty | string | Order amount as a formatted string. | yes | 88.00 |
| status | string(enum) | Order status. In the current implementation, the webhook status is PAID, EXPIRED, CANCEL, or REFUND. | yes | PAID |
| expired | integer | Order expiration timestamp in milliseconds. | yes | 1760000600000 |
| created | integer | Order creation timestamp in milliseconds. | yes | 1760000000000 |
| updated | integer | Order last update timestamp in milliseconds. | yes | 1760000300000 |
Signature Verification
- AinePay signs "the URL-encoded webhook body +
×tamp=<ts>&recvWindow=<rw>" with the merchant's webhook verification key. - The signature is sent in
x-api-signatureas lowercase hex; the timestamp and window are sent inx-api-timestampandx-api-recv-windowrespectively, and both participate in the HMAC. - The body portion of the signing input is the canonical URL-encoded form string sorted by field name. The trailing
timestamp/recvWindowvalues are appended as raw digits and are not URL-encoded. - Merchant verification steps:
- Read
x-api-timestampandx-api-recv-window; reject the request when|now - timestamp| > min(recvWindow, MAX_ACCEPTABLE_RECV_WINDOW)(aMAX_ACCEPTABLE_RECV_WINDOWof300000ms is recommended). - Re-parse and re-encode the body in alphabetical order to obtain the canonical body.
- Append
×tamp=<ts>&recvWindow=<rw>, compute HMAC-SHA256, and compare againstx-api-signaturewith constant-time comparison.
- Read
- Because both
timestampandrecvWindoware signed, an attacker cannot tamper with either header without invalidating the signature. - For Java / TypeScript webhook verification example code, see Authentication.
Example Request
POST /ainepay/notify HTTP/1.1 Host: merchant.example.com Content-Type: application/x-www-form-urlencoded x-api-signature: <hex(HMAC-SHA256(notifySecret, body + "×tamp=" + ts + "&recvWindow=" + rw))> x-api-timestamp: 1760000300000 x-api-recv-window: 5000 chain=ETH&coin=USDT&created=1760000000000&expired=1760000600000&merchantId=20001&orderId=ORDER_10001&qty=88.00&status=PAID&updated=1760000300000&userId=U_90001
Expected Response
| field | type | description |
|---|---|---|
| HTTP status | integer | Return any 2xx status to acknowledge successful receipt. |
HTTP/1.1 200 OK Content-Type: text/plain ok
Notes
- Handle the webhook idempotently by
orderId. - Treat the webhook as a status update signal, then use Query Orders to confirm the latest order status before executing your final business action.
- If the signature is invalid, do not trust the payload.