Payment Callback
After payment succeeds or fails, Velora sends a POST request to the notifyUrl you provided in Create payment order. The body is JSON. Implement this endpoint on your server and verify the signature and process the result.
When we call
- Order succeeds/fails
The same order may be notified more than once due to retries. Your server must be idempotent.
Delivery rules
- A
2xxresponse is treated as success and stops delivery retries. - Non-
2xx, timeout, network issues, or signature generation errors will trigger retries. - Callback delivery stops after the maximum retry count is reached.
Attempts and backoff
We attempt delivery up to 8 times in total (including the first attempt), using exponential backoff with a base delay of 15s. Retry intervals grow from 15s up to roughly 16min, with total retry waiting time around 31m 45s.
Typical wait intervals after each failure (for reference only):
- After 1st failure: 15s
- After 2nd failure: 30s
- After 3rd failure: 60s
- After 4th failure: 120s
- After 5th failure: 240s
- After 6th failure: 480s
- After 7th failure: 960s (about 16min)
Request (Velora → your server)
- Method:
POST - Content-Type:
application/json - Body: JSON; see parameters below
Signature verification
To ensure callback requests are authentic and untampered, verify the callback signature on your server. The signature verification algorithm is the same as the algorithm used when creating a payment request:
- Use
x-timestampand the raw callback body to build the signing string astimestamp + "." + body. - Compute HMAC-SHA256 with your
appSecret, then compare withx-signature(sha256=<hex>). - Run business logic only after verification succeeds; if verification fails, return non-
2xxso the platform retries based on delivery rules.
For signing details and required headers, see Authentication.
Callback headers
| Header | Type | Description |
|---|---|---|
X-Signature | string | Callback signature in sha256=<hex> format |
X-Timestamp | string | Unix timestamp in seconds |
Content-Type | string | Always application/json |
X-Merchant-Id | string | Merchant ID |
X-Transaction-Id | string | Platform transaction ID (payment ID) |
Headers example
{
"X-Signature": "sha256=e94f6d2853a5e7f702214e5e0292e45cbb486b9e41f610166313568683bf5805",
"X-Timestamp": "1774948696",
"Content-Type": "application/json",
"X-Merchant-Id": "M1770606222033126",
"X-Transaction-Id": "payin_20260331171813721136740"
}Callback body
The callback body is JSON with result (payment outcome) and data (order details). Payment is successful only when result.code === "S0000" and data.status === "Success".
result object
| Field | Type | Description |
|---|---|---|
code | string | Response code (success/failure) |
msg | string | Message |
data object
| Field | Type | Length / limits | Always returned | Description |
|---|---|---|---|---|
paymentId | string | / | Yes | Velora platform payment ID |
merchantOrderId | string | / | No | Merchant order ID |
psp | string | / | No | PSP identifier that actually processes the order |
channelOrderId | string | / | No | Order ID on the PSP / channel side |
status | string | / | Yes | Current payment status; see Order status |
amount | object | / | No | Amount; see below |
paymentMethod | object | / | No | Payment method; see below |
createdAt | string | / | No | Created at |
updatedAt | string | / | No | Updated at |
data.amount object
| Field | Type | Length / limits | Always returned | Description |
|---|---|---|---|---|
value | string | / | Yes when amount is present | Amount as string, e.g. "29.00" |
currency | string | / | Yes when amount is present | Currency code |
data.paymentMethod object
| Field | Type | Length / limits | Always returned | Description |
|---|---|---|---|---|
type | string | / | No | Payment method type |
paymentBrand | string | / | No | Payment brand |
Example body
{
"result": {
"code": "S0000",
"msg": "Payment query successfully"
},
"data": {
"paymentId": "payin_20260331145719282948882",
"merchantOrderId": "ORDER_20260331_145711_1LOQON",
"psp": "gcash_partner",
"channelOrderId": "GCASH20260331145719282948882",
"status": "Success",
"amount": {
"value": "29.00",
"currency": "PHP"
},
"paymentMethod": {
"type": "e-wallet",
"paymentBrand": "gcash"
},
"createdAt": "2026-03-31T14:57:19+08:00",
"updatedAt": "2026-03-31T15:00:12+08:00"
}
}Crypto callback response structure
For cryptocurrency collection scenarios, the callback response differs from standard payment flows mainly because paymentMethod includes an additional crypto object that describes the on-chain transaction details.
data.paymentMethod.crypto object
| Field | Type | Always returned | Description |
|---|---|---|---|
chain | string | Yes | Chain type, such as ETH, BSC, or TRON |
fromAddress | string | Yes | Wallet address from which the user initiated the transfer |
toAddress | string | Yes | Merchant collection wallet address |
logIndex | string | Yes | On-chain event log index |
txHash | string | Yes | On-chain transaction hash |
Crypto callback response example
Note: Merchants do not need to strictly use all fields returned by this callback. They can use the JSON structure under the crypto field to implement their own generic business processing.
Merchants must perform a uniqueness check on txHash + logIndex to avoid processing the same on-chain event more than once.
{
"result": {
"code": "S0000",
"msg": "Payment query successfully"
},
"data": {
"psp": "cryptopay",
"status": "Success",
"paymentId": "payin_20260528104200903337225",
"channelOrderId": "0xd9ca31dd9f6f12077fd3525acb6706de610ae0c2c30315300e51f74f0cbd91b6_0",
"channelMerchantOrderId": "0xd9ca31dd9f6f12077fd3525acb6706de610ae0c2c30315300e51f74f0cbd91b6_0",
"merchantOrderId": "0xd9ca31dd9f6f12077fd3525acb6706de610ae0c2c30315300e51f74f0cbd91b6",
"amount": {
"value": "1.36000",
"currency": "USDT"
},
"paymentMethod": {
"type": "crypto",
"paymentBrand": "BSC",
"crypto": {
"chain": "BSC",
"fromAddress": "0x38c9e15b7a28038f74290187e2747954ee507411",
"toAddress": "0x8be0f2577b0018844ff19b675f387cc36605f1b7",
"logIndex": "0",
"txHash": "0xd9ca31dd9f6f12077fd3525acb6706de610ae0c2c30315300e51f74f0cbd91b6"
}
},
"createdAt": "2026-05-28T10:42:00+08:00",
"updatedAt": "2026-05-28T10:42:00+08:00"
}
}Your response
After you finish callback processing (for example, update order status and fulfill), return HTTP 2xx. Any non-2xx response is treated as failure and will be retried.
Recommended flow
- Parse body; if the platform uses signature verification, verify per platform rules; if invalid, return 400.
- Idempotency: Check by
data.merchantOrderId(ordata.paymentId) whether you already processed this order; if yes, return 200. - Business logic: Update order status, fulfill order, etc. Treat as payment success only when
result.code === "S0000"anddata.status === "Success"; only then perform fulfillment, settlement, etc. - Return 200 to avoid duplicate callbacks.
Security
- Handle callbacks only on the server; do not rely on frontend or untrusted requests.
- Verify amount and order ID match your local order to prevent tampering.
See also
- Create payment order: Where
notifyUrlis set - Query payment order: Active status check when needed
- Response codes: API response code reference