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 URLhttps://api.kontakpay.com
FormatJSON over HTTPS. Send Content-Type: application/json.
AuthenticationAuthorization: Bearer <API key> — see Authentication
CurrencyINR only

How it works

  1. Create a payin with the amount and your own order_id. You get a payment_link (our hosted payment page) and raw UPI links.
  2. Show the payment — open the payment_link, embed it in an iframe, or build your own UI with the UPI links / QR string.
  3. Receive a webhook (PAYIN_SUCCESS, PAYIN_FAILED or PAYIN_EXPIRED) and fulfil the order. You can also poll Get a payin.
  4. 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 returns 401 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".

SentResult
1500 / "1500.00"Accepted → "1500.00"
1500.001400 INVALID_REQUEST — more than two decimals
0 / -10400 INVALID_REQUEST — must be positive
Outside your limits422 AMOUNT_LIMIT — the message states the allowed range

IDs

IDFormatNotes
Payinpin_…Our unique ID for a payin.
Payoutpout_…Our unique ID for a payout.
order_idYours, up to 128 charactersReturned 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_rrnBank referencePresent 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 200 instead of creating a new one.
  • Same key, different body → 409 DUPLICATE_IDEMPOTENCY_KEY.
  • Keys are kept for 24 hours, per merchant.
Always send an idempotency_key for payouts — it guarantees a retried request never pays twice.
POST/v1/payin/create

Create a payin

Collect a UPI payment. The customer pays with any UPI app; the result arrives by webhook.

payment_modeWhat you get
intentApp links: upi (any UPI app), gpay, phonepe, paytm
qrA 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).

🔒 Security
Bearer API key — see Authentication
Body application/json required
payment_modestring required
Enum: "intent""qr"
amountnumber | string required

INR amount, at most two decimals — e.g. 1500.00. Must be within your account limits.

order_idstring required

Your order or reference ID, up to 128 characters.

idempotency_keystring

Optional. See Idempotency.

customerobject

Optional customer details.

customer.namestring

Up to 120 characters.

customer.emailstring
customer.mobilestring

10-digit Indian mobile number.

Responses

Payin created. 200 is returned instead when an idempotency_key replays an earlier payin.

GET/v1/payin/{payin_id}

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.

Path parameters
payin_idstring required

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.

GET/v1/payin?order_id=

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.

Query parameters
order_idstring required

Responses

data is an array of payin objects (see Get a payin). Empty when nothing matches.

POST/v1/payout/create

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.

StatusMeaning
PENDINGAccepted, being sent to the bank
PROCESSINGWith the bank
SUCCESSDelivered — bank_rrn is set
FAILEDRejected — failure_reason is set and the amount is back in your payout balance
Payouts must be enabled on your account. Otherwise this endpoint returns 403 PAYOUT_DISABLED and your payout balance reads 0.00.
🔒 Security
Bearer API key — see Authentication
Body application/json required
amountnumber | string required

INR amount, at most two decimals.

order_idstring required

Your reference for this payout, up to 128 characters.

idempotency_keystring

Strongly recommended — see Idempotency.

bank_accountobject required
bank_account.holder_namestring required

Account holder name, up to 120 characters.

bank_account.account_numberstring required

6–20 digits.

bank_account.ifsc_codestring required

11-character IFSC, e.g. HDFC0001234.

bank_account.mobilestring

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.

GET/v1/payout/{payout_id}

Get a payout

Returns the payout in its current state.

Path parameters
payout_idstring required

e.g. pout_4fe91358-d00f-4f4d-a14a-a60f15378468

Responses

GET/v1/payout?order_id=

List payouts by order ID

All payouts created with the given order_id, newest first (up to 50).

Query parameters
order_idstring required

Responses

data is an array of payout objects.

GET/v1/balance

Get balance

Your current balances in INR.

BalanceMeaning
payout_balanceAvailable for payouts. When payouts are enabled, successful payins are credited here automatically. 0.00 when payouts are not enabled.
payin_balanceWhen payouts are not enabled, successful payins collect here and are settled to you in USDT (TRC20).

Responses

currencystring
Value: "INR"
payin_balancestring
payout_balancestring

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.

EventWhen
PAYIN_SUCCESSThe customer paid
PAYIN_FAILEDThe payment failed
PAYIN_EXPIREDNot paid before expires_at
PAYOUT_PROCESSINGThe payout is with the bank
PAYOUT_SUCCESSThe payout was delivered
PAYOUT_FAILEDThe payout failed; the amount is back in your payout balance

Payload

data is exactly what Get a payin / Get a payout returns.

Headers

HeaderDescription
x-webhook-eventEvent name, e.g. PAYIN_SUCCESS
x-webhook-delivery-idUnique per event (evt_…) — use it to ignore duplicates
x-webhook-timestampUnix time in seconds when the request was signed
x-webhook-signatureHex 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-id and trust the status in data, not the arrival order.
  • Webhook URLs must be public https addresses.
  • 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.

FieldDescription
typeAlways "kontakpay:payin"
payin_id, order_idWhich payment
statusCREATED, SUCCESS, FAILED or EXPIRED
Inside an iframe the UPI app buttons open in the top window, so they work on mobile browsers that block app links from frames.

Errors

Errors return an HTTP status and a JSON body:

error_codeHTTPWhen
INVALID_REQUEST400A field is missing or malformed (field says which)
INVALID_JSON400The body is not a JSON object
UNAUTHORIZED401API key missing or unknown
KEY_REVOKED401The API key was revoked or rotated
ACCOUNT_DISABLED403Your merchant account is disabled
PAYIN_DISABLED403Payins are not enabled on your account
PAYOUT_DISABLED403Payouts are not enabled on your account
MODE_NOT_SUPPORTED403The payment_mode is not available for your account
PROVIDER_OFFLINE403Payments are temporarily unavailable
NOT_FOUND404Unknown ID
DUPLICATE_IDEMPOTENCY_KEY409Same idempotency_key, different body
AMOUNT_LIMIT422Amount outside your limits
LOW_BALANCE422Payout balance too low
PROVIDER_ERROR502The payment network refused the request — safe to retry
PROVIDER_TIMEOUT504The payment network did not respond — retry with the same idempotency_key
INTERNAL_ERROR500Something went wrong on our side