Skip to content

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 2xx response 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-timestamp and the raw callback body to build the signing string as timestamp + "." + body.
  • Compute HMAC-SHA256 with your appSecret, then compare with x-signature (sha256=<hex>).
  • Run business logic only after verification succeeds; if verification fails, return non-2xx so the platform retries based on delivery rules.

For signing details and required headers, see Authentication.

Callback headers

HeaderTypeDescription
X-SignaturestringCallback signature in sha256=<hex> format
X-TimestampstringUnix timestamp in seconds
Content-TypestringAlways application/json
X-Merchant-IdstringMerchant ID
X-Transaction-IdstringPlatform transaction ID (payment ID)

Headers example

json
{
  "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

FieldTypeDescription
codestringResponse code (success/failure)
msgstringMessage

data object

FieldTypeLength / limitsAlways returnedDescription
paymentIdstring/YesVelora platform payment ID
merchantOrderIdstring/NoMerchant order ID
pspstring/NoPSP identifier that actually processes the order
channelOrderIdstring/NoOrder ID on the PSP / channel side
statusstring/YesCurrent payment status; see Order status
amountobject/NoAmount; see below
paymentMethodobject/NoPayment method; see below
createdAtstring/NoCreated at
updatedAtstring/NoUpdated at

data.amount object

FieldTypeLength / limitsAlways returnedDescription
valuestring/Yes when amount is presentAmount as string, e.g. "29.00"
currencystring/Yes when amount is presentCurrency code

data.paymentMethod object

FieldTypeLength / limitsAlways returnedDescription
typestring/NoPayment method type
paymentBrandstring/NoPayment brand

Example body

json
{
  "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

FieldTypeAlways returnedDescription
chainstringYesChain type, such as ETH, BSC, or TRON
fromAddressstringYesWallet address from which the user initiated the transfer
toAddressstringYesMerchant collection wallet address
logIndexstringYesOn-chain event log index
txHashstringYesOn-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.

json
{
  "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.

  1. Parse body; if the platform uses signature verification, verify per platform rules; if invalid, return 400.
  2. Idempotency: Check by data.merchantOrderId (or data.paymentId) whether you already processed this order; if yes, return 200.
  3. Business logic: Update order status, fulfill order, etc. Treat as payment success only when result.code === "S0000" and data.status === "Success"; only then perform fulfillment, settlement, etc.
  4. 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