Webhook (Async Callback) Guide
SoPay sends an HTTP POST to your notifyUrl (or default callbackUrl) when a payin or payout order reaches status=completed.
Verify signature, do not decrypt. The JSON body is plaintext. The platform signs with its private key; you verify
X-Signwith the platform public key.
Keys
| Direction | Signs with | Verify with |
|---|---|---|
| Merchant → Platform (API calls) | Merchant private key | Platform uses merchant public key |
| Platform → Merchant (webhooks) | Platform private key | Merchant uses platform public key |
Obtain the platform public key from the merchant portal (Profile → Security → Platform RSA public key, or GET /portal/profile → platformPublicKey).
When webhooks fire
| Order | Trigger | URL |
|---|---|---|
| Payin | status=completed | notifyUrl on create; no webhook if empty |
| Payout | status=completed | notifyUrl or merchant default callbackUrl |
Duplicate deliveries are possible — handle idempotently by merchantOrderNo / orderNo.
HTTP format
- POST,
Content-Type: application/json - Headers:
X-Merchant-No,X-Timestamp(Unix seconds),X-Nonce,X-Sign(Base64 RSA2)
Body fields
| Field | Type | Notes |
|---|---|---|
orderType | string | payin or payout |
orderNo | string | Platform order id |
merchantOrderNo | string | Your order id |
status | string | completed on success webhooks |
amount | string | Order amount |
feeAmount | string | Fee |
netAmount | string | Payin: credit amount; Payout: total debit incl. fee |
completedAt | number | Unix time in seconds |
Time fields (API & webhooks)
All merchant-facing time values use Unix timestamps in seconds (JSON number), including:
- Open API responses:
createdAt,completedAt - Webhook body:
completedAt - Webhook headers:
X-Timestamp
Admin / portal list APIs for merchant orders, wallet transactions, and callbacks use the same convention.
Signature verification
- Flatten top-level JSON body to
map[string]string - Add
timestampandnoncefrom headers - Sort keys ASCII ascending, join
k=v&... - RSA2-verify
X-Signwith platform public key
Body flattening
| JSON type | Signed value |
|---|---|
string | As-is (empty omitted) |
number (e.g. completedAt) | Decimal integer string, e.g. 1782121882 (no scientific notation) |
object / array | JSON string |
Worked example (with completedAt)
Sign string:
amount=2000&completedAt=1782121882&feeAmount=6&merchantOrderNo=h7z3jskx7h7j8j&netAmount=1994&nonce=e63b411b-f379-4e69-9de8-d58c192d3e90&orderNo=PI202606220949069037e0&orderType=payin&status=completed×tamp=1782121883Use portal API Debug → Callback verify to compare sign strings locally.
ACK & retries
Respond HTTP 200 with body exactly OK after verify + successful business update. Otherwise the platform retries (up to ~200 attempts). See Chinese guide for full Go/JS/Python samples.
