Vela Pay API: integration guide
Integration guide and API reference for Vela Pay. Start with "Getting started", then create a payment and handle callbacks.
Getting started
Environments
| Base URL | |
|---|---|
| Sandbox | https://sandbox.velapay.io |
| Production | https://api.velapay.io |
Create API keys in the merchant cabinet (https://merchant.velapay.io, Settings). Every request carries the key in Authorization: Bearer <key> or X-Api-Key: <key>. Requests are accepted only from the IP addresses allow-listed for the key. Keys can be rotated in the cabinet at any time; the previous key keeps working until you revoke it.
All amounts are strings with two decimals in INR. Timestamps are ISO 8601 in UTC.
Create a payment
Call POST /payment with the order amount, your own order reference and the webhook URL that will receive status updates.
{
"settle_sum": "1250.00",
"settle_ccy": "INR",
"order_token": "order-2026-000123",
"notify_hook": "https://merchant.example/hooks/payments",
"rail_kind": "p2p_send"
}
order_tokenmakes the call idempotent: repeating a request with the same value returns the original payment instead of creating a second one. Use your order id.notify_hookis required. It must be an HTTPS URL reachable from the internet.- The response contains
movement_id(store it),movement_stateandpay_link: redirect the payer to that page. It shows the payment details, opens the payer's UPI app and collects the confirmation. You do not need to render anything yourself.
Payment lifecycle
A payment moves through the states below. Poll GET /{id}/status (no more often than every 5 seconds) or, better, rely on the callback and use polling only as a fallback.
| State | Meaning | Next step |
|---|---|---|
on_hold |
awaiting the payer; the payer is completing the transfer | intermediate, keep polling or wait for the callback |
processed |
awaiting the payer; funds received | intermediate, keep polling or wait for the callback |
unsuccessful |
not completed | final |
discarded |
not completed; the payment window closed | final |
sent_back |
returned to the payer; awaiting the payer | intermediate, keep polling or wait for the callback |
contested |
disputed | final |
Only final states are stable. Never treat an intermediate state as paid. net_amount is the amount actually received and cleared_time the time the funds were confirmed.
Callbacks
Every state change is delivered with POST to the notify_hook of the payment. The body has the same shape as the status response.
Each delivery carries X-Webhook-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of the string <t>.<raw request body> computed with the webhook secret issued to you at onboarding (keep it out of your client-side code). Recompute it over the raw body exactly as received, compare in constant time and reject deliveries whose t is older than five minutes. If no webhook secret was issued for your account, the header is absent and you must fetch the payment state with the status endpoint before acting on a callback.
Respond with any 2xx status within 10 seconds. Any other response or a timeout is retried with increasing delays for about 32 hours, so your handler must be idempotent: the same event can arrive more than once. Process by movement_id and the state, not by delivery order.
Payer confirmation and receipts
On the hosted payment page the payer completes the transfer in a UPI app or by bank transfer and then confirms it either with the 12-digit bank reference (UTR) or by uploading a receipt. Vela Pay matches the confirmation against the incoming funds; the payment stays in an intermediate state until the match is complete and then moves to a final state, which you receive through the callback.
If you collect the bank reference yourself, submit it with POST /confirm together with movement_id. A reference that does not match, was already used or belongs to a different amount ends the payment with a final failure state and decline_note explaining why; the payer is offered to upload a receipt or to contact support on the page.
Errors
Errors are returned with an HTTP status and a stable code. Use the code, not the message, in your logic.
| Code | HTTP | Message |
|---|---|---|
VL_FUNDS_SHORT |
402 | The payer does not have enough balance to complete this inflow. |
VL_MOVEMENT_MISSING |
404 | No movement was found for the supplied reference. |
VL_ENTRY_HELD |
423 | This movement is held while an adjustment is being applied. |
VL_SUM_OUT_OF_RANGE |
422 | The settle amount is outside the range allowed for this rail. |
VL_TOKEN_REUSED |
409 | A movement already exists for this order token. |
VL_CCY_NOT_ALLOWED |
422 | This account settles in INR only. |
VL_RAIL_SILENT |
502 | The upstream rail gave no response; please retry shortly. |
VL_RAIL_OFF |
422 | The chosen rail is not enabled for this merchant. |
VL_RAIL_PAUSED |
503 | This rail is paused for maintenance right now. |
VL_CHANNEL_MISMATCH |
422 | The channel is not valid for the selected rail. |
VL_ACCOUNT_BARRED |
403 | This merchant account may not transact. |
VL_SOURCE_ACCOUNT_BAD |
422 | The sender account details did not pass validation. |
VL_SOURCE_NAME_BAD |
422 | The sender name is missing or malformed. |
VL_CORE_BROKE |
500 | An internal fault occurred; the movement was not created. |
VL_OVER_CAPACITY |
503 | The gateway is at capacity; please retry shortly. |
VL_STATE_BROKEN |
500 | The movement reached an inconsistent state and was rejected. |
VL_STATE_CLASH |
409 | The requested action conflicts with the current movement state. |
VL_TOO_FAST |
429 | Too many requests; slow down and retry. |
VL_SECRETS_DENIED |
403 | The supplied rail secrets were rejected. |
VL_VELOCITY_HIT |
429 | A velocity limit was hit for this account. |
VL_HOOK_BAD |
422 | The notify hook must be a public HTTPS URL. |
VL_RETURN_BAD |
422 | The return link is missing or invalid. |
VL_WIDGET_UNKNOWN |
422 | The requested widget kind is not recognised. |
VL_CONCURRENCY_HIT |
429 | Too many concurrent operations on this movement. |
VL_TOKEN_BAD |
422 | The order token is missing or malformed. |
VL_ALREADY_SETTLED |
409 | This movement has already settled. |
VL_SETTLE_FAULT |
500 | Settlement could not be recorded; support has been notified. |
VL_ADJUST_BAD |
422 | The adjustment amount is not allowed for this movement. |
VL_RECALL_BAD |
422 | This movement is not eligible for a send-back. |
VL_PAYLOAD_BAD |
422 | The request payload failed schema validation. |
VL_UPSTREAM_ODD |
500 | An upstream provider returned an unexpected response. |
VL_TIMED_OUT |
500 | The operation timed out before the rail confirmed. |
VL_LIMIT_HIT |
422 | The amount breaches a configured movement limit. |
VL_UNSORTED_FAULT |
500 | An unclassified processing error occurred. |
VL_UNEXPECTED |
500 | An unexpected error occurred while processing the request. |
Validation problems (missing fields, wrong types) come back as 400 with a list of fields. 401 means the key or the source IP is not accepted. 5xx responses are safe to retry with the same order_token.
Sandbox and testing
Use the sandbox base URL with sandbox keys from https://sandbox-merchant.velapay.io. Payments there never move real money: the payment page lets you complete or fail a payment on demand, so you can test every state, the callback signature and your retry handling. Check that your endpoint answers 2xx and that repeated deliveries do not create duplicate orders.
Reconciliation
The merchant cabinet provides a settlement report (CSV or XLSX) for any period with the bank reference of every payment, gross amount, fees and net amount. Match the report against your bank statement by the bank reference. Payouts show the same breakdown per settlement.
API reference
Machine-readable OpenAPI: openapi.json next to this guide. Endpoints: /payment, /{id}/status, /confirm.