KontakPay API
Accept UPI payments from customers in India (payins) and send money to Indian bank accounts (payouts) through one REST API. All amounts are in INR.
| Base URL | https://api.kontakpay.com |
|---|---|
| Format | JSON over HTTPS. Send Content-Type: application/json. |
| Authentication | Authorization: Bearer <API key> — see Authentication |
| Currency | INR only |
How it works
- Create a payin with the amount and your own
order_id. You get apayment_link(our hosted payment page) and raw UPI links. - Show the payment — open the
payment_link, embed it in an iframe, or build your own UI with the UPI links / QR string. - Receive a webhook (
PAYIN_SUCCESS,PAYIN_FAILEDorPAYIN_EXPIRED) and fulfil the order. You can also poll Get a payin. - Send payouts (if enabled on your account) from your payout balance to any Indian bank account.
Response format
Every successful response is wrapped in { "success": true, "data": … }. Errors return { "success": false, "error_code", "message", "field" } — see Errors. Fields only appear when they carry a value; there are no null placeholders.
Authentication
Every request carries your API key as a Bearer token:
Create and manage keys in the merchant dashboard under API Keys. Each key has its own webhook secret, shown once together with the key.
- Keep keys on your server. Never put them in a browser or mobile app.
- Rotate a key from the dashboard if it leaks: the old key and its webhook secret stop working immediately. Webhook URLs are kept.
- A missing or unknown key returns
401 UNAUTHORIZED; a revoked key returns401 KEY_REVOKED.
Amounts & IDs
Amounts
Amounts are INR with at most two decimals. In requests you can send a number or a string — 500, 500.5, 500.00 and "500.00" are all accepted. Responses always return a string with two decimals, e.g. "1500.00".
| Sent | Result |
|---|---|
1500 / "1500.00" | Accepted → "1500.00" |
1500.001 | 400 INVALID_REQUEST — more than two decimals |
0 / -10 | 400 INVALID_REQUEST — must be positive |
| Outside your limits | 422 AMOUNT_LIMIT — the message states the allowed range |
IDs
| ID | Format | Notes |
|---|---|---|
| Payin | pin_… | Our unique ID for a payin. |
| Payout | pout_… | Our unique ID for a payout. |
order_id | Yours, up to 128 characters | Returned on every response and webhook. It does not have to be unique — use List by order ID to find all attempts for an order. |
bank_rrn | Bank reference | Present once a payin or payout succeeds. |
Idempotency
Send an optional idempotency_key (up to 128 characters) in the body of Create a payin and Create a payout to retry safely after a timeout or network error.
- Same key, same body → the original record is returned with
200instead of creating a new one. - Same key, different body →
409 DUPLICATE_IDEMPOTENCY_KEY. - Keys are kept for 24 hours, per merchant.
idempotency_key for payouts — it guarantees a retried request never pays twice.Create a payin
Collect a UPI payment. The customer pays with any UPI app; the result arrives by webhook.
payment_mode | What you get |
|---|---|
intent | App links: upi (any UPI app), gpay, phonepe, paytm |
qr | A qr string — encode it as a QR code |
In both modes payment_link is our hosted payment page, which handles everything and can be embedded in an iframe. A payin stays payable until expires_at (15 minutes by default).
"intent""qr"INR amount, at most two decimals — e.g. 1500.00. Must be within your account limits.
Your order or reference ID, up to 128 characters.
Optional customer details.
Up to 120 characters.
10-digit Indian mobile number.
Responses
Payin created. 200 is returned instead when an idempotency_key replays an earlier payin.
INVALID_REQUEST (a field is missing or malformed — field says which) or INVALID_JSON.
PAYIN_DISABLED, ACCOUNT_DISABLED, MODE_NOT_SUPPORTED or PROVIDER_OFFLINE.
DUPLICATE_IDEMPOTENCY_KEY — the key was used with a different body.
AMOUNT_LIMIT — the amount is outside your limits.
Get a payin
Returns the payin in its current state. Use it to check a payment when you did not receive (or want to confirm) a webhook.
e.g. pin_c15497e7-d8a1-4de6-9f0b-8f4ab44a39f9
Responses
Fields depend on status: CREATED has the payment links and expires_at; SUCCESS has bank_rrn and paid_at; FAILED has failure_reason and failed_at; EXPIRED has expired_at.
NOT_FOUND — no payin with this ID on your account.
List payins by order ID
All payins created with the given order_id, newest first (up to 50). Useful when a customer retried a payment.
Responses
data is an array of payin objects (see Get a payin). Empty when nothing matches.
INVALID_REQUEST — order_id is missing.
Create a payout
Send money from your payout balance to an Indian bank account. The amount is reserved from your balance when the payout is created and refunded automatically if it fails.
| Status | Meaning |
|---|---|
PENDING | Accepted, being sent to the bank |
PROCESSING | With the bank |
SUCCESS | Delivered — bank_rrn is set |
FAILED | Rejected — failure_reason is set and the amount is back in your payout balance |
403 PAYOUT_DISABLED and your payout balance reads 0.00.INR amount, at most two decimals.
Your reference for this payout, up to 128 characters.
Account holder name, up to 120 characters.
6–20 digits.
11-character IFSC, e.g. HDFC0001234.
Optional 10-digit Indian mobile number.
Responses
Payout created — usually PENDING. If the bank rejects it on the spot it is returned as FAILED (still 201) and the amount is refunded.
INVALID_REQUEST — e.g. an invalid IFSC or account number; field says which.
PAYOUT_DISABLED — payouts are not enabled on your account.
DUPLICATE_IDEMPOTENCY_KEY.
LOW_BALANCE — your payout balance does not cover the payout; or AMOUNT_LIMIT.
Get a payout
Returns the payout in its current state.
e.g. pout_4fe91358-d00f-4f4d-a14a-a60f15378468
Responses
NOT_FOUND.
List payouts by order ID
All payouts created with the given order_id, newest first (up to 50).
Responses
data is an array of payout objects.
INVALID_REQUEST — order_id is missing.
Get balance
Your current balances in INR.
| Balance | Meaning |
|---|---|
payout_balance | Available for payouts. When payouts are enabled, successful payins are credited here automatically. 0.00 when payouts are not enabled. |
payin_balance | When payouts are not enabled, successful payins collect here and are settled to you in USDT (TRC20). |
Responses
"INR"Webhooks
Set a webhook URL on your API key in the dashboard (optionally a separate payout webhook URL). We POST an event there every time a payin or payout reaches a new status. The URL is copied onto each payment when it is created, so changing it later never loses a notification.
| Event | When |
|---|---|
PAYIN_SUCCESS | The customer paid |
PAYIN_FAILED | The payment failed |
PAYIN_EXPIRED | Not paid before expires_at |
PAYOUT_PROCESSING | The payout is with the bank |
PAYOUT_SUCCESS | The payout was delivered |
PAYOUT_FAILED | The payout failed; the amount is back in your payout balance |
Payload
data is exactly what Get a payin / Get a payout returns.
Headers
| Header | Description |
|---|---|
x-webhook-event | Event name, e.g. PAYIN_SUCCESS |
x-webhook-delivery-id | Unique per event (evt_…) — use it to ignore duplicates |
x-webhook-timestamp | Unix time in seconds when the request was signed |
x-webhook-signature | Hex HMAC-SHA256 of <timestamp>.<raw body> using your webhook secret |
Verify the signature
Compute the HMAC over the raw request body (before any JSON parsing), compare in constant time, and reject requests older than 5 minutes.
Retries & best practices
Reply with any 2xx within 10 seconds. Otherwise we retry up to 6 more times: 30s → 5m → 30m → 2h → 6h → 24h. Redirects are not followed.
- Respond first, then process — keep the handler fast.
- Events can arrive more than once or out of order. De-duplicate on
x-webhook-delivery-idand trust thestatusindata, not the arrival order. - Webhook URLs must be public
httpsaddresses. - Failed deliveries can be retried from the dashboard under Webhooks.
Payment page & iframe
payment_link opens our hosted payment page: UPI app buttons or a QR code, a countdown, and the result — updated live. Redirect the customer to it, or embed it in your checkout:
When the payment finishes, the page sends a message to the parent window. Use it to update your UI; always confirm with the webhook (or Get a payin) before fulfilling the order.
| Field | Description |
|---|---|
type | Always "kontakpay:payin" |
payin_id, order_id | Which payment |
status | CREATED, SUCCESS, FAILED or EXPIRED |
Errors
Errors return an HTTP status and a JSON body:
| error_code | HTTP | When |
|---|---|---|
INVALID_REQUEST | 400 | A field is missing or malformed (field says which) |
INVALID_JSON | 400 | The body is not a JSON object |
UNAUTHORIZED | 401 | API key missing or unknown |
KEY_REVOKED | 401 | The API key was revoked or rotated |
ACCOUNT_DISABLED | 403 | Your merchant account is disabled |
PAYIN_DISABLED | 403 | Payins are not enabled on your account |
PAYOUT_DISABLED | 403 | Payouts are not enabled on your account |
MODE_NOT_SUPPORTED | 403 | The payment_mode is not available for your account |
PROVIDER_OFFLINE | 403 | Payments are temporarily unavailable |
NOT_FOUND | 404 | Unknown ID |
DUPLICATE_IDEMPOTENCY_KEY | 409 | Same idempotency_key, different body |
AMOUNT_LIMIT | 422 | Amount outside your limits |
LOW_BALANCE | 422 | Payout balance too low |
PROVIDER_ERROR | 502 | The payment network refused the request — safe to retry |
PROVIDER_TIMEOUT | 504 | The payment network did not respond — retry with the same idempotency_key |
INTERNAL_ERROR | 500 | Something went wrong on our side |