/v1 on this host. JSON in, JSON out.key_test_…) run against the bank simulator. Live keys move real money and need approved KYC.49900.Idempotency-Key header on every POST. Retrying with the same key returns the original response instead of creating a second transaction.{"error": {"code": "…", "message": "…"}} with a 4xx/5xx status.curl -u key_test_xxx:sk_test_xxx -X POST https://YOUR_HOST/v1/payments \
-H 'Content-Type: application/json' -H 'Idempotency-Key: order-1001-attempt-1' \
-d '{
"order_id": "ORD-1001",
"amount": 49900,
"description": "Blue cotton shirt",
"customer": {"name": "Asha", "email": "[email protected]", "phone": "9876543210"},
"redirect_url": "https://yourstore.in/order/ORD-1001/done"
}'
| Field | Notes |
|---|---|
order_id | Required. Your unique reference, 1-64 chars. Reusing one returns 409 duplicate_order_id. |
amount | Required. Paise, minimum 100. |
flow | intent (default: QR + UPI app link) or collect (needs payer_vpa). |
redirect_url | Where the customer returns after paying. We append payment_id, order_id, status. |
expires_in | Seconds until the payment expires. 60-86400, default 900. |
notes | Up to 15 string key/values, returned on the payment and in webhooks. |
checkout_urlThe response includes checkout_url (our hosted page with QR and UPI app button) and upi_uri if you want to render your own QR.
Never trust the redirect alone. Mark the order paid only when you receive a verified payment.success webhook, or when GET /v1/payments/{id} returns "status": "success" with the amount you expect.
Statuses: pending → success | failed | expired.
GET /v1/payments/{id}
GET /v1/payments?order_id=ORD-1001&status=success&limit=25&offset=0
POST /v1/payments/{id}/refunds {"amount": 10000, "reason": "Size issue"} # omit amount for a full refund
GET /v1/payments/{id}/refunds
GET /v1/refunds/{refund_id}
Refunds start as processing and finish as success or failed (webhook: refund.success / refund.failed). The refund is debited from your pending balance if the payment is unsettled, otherwise from your available balance. Fees on the original payment are not returned.
curl -u key_test_xxx:sk_test_xxx -X POST https://YOUR_HOST/v1/payouts \
-H 'Content-Type: application/json' -H 'Idempotency-Key: payout-7781' \
-d '{
"reference_id": "VENDOR-7781",
"amount": 250000,
"beneficiary": {"name": "Sharma Traders", "vpa": "sharma.traders@okhdfc"},
"purpose": "vendor payment"
}'
vpa (rail UPI, up to ₹1,00,000) or account_number + ifsc (rail IMPS up to ₹5,00,000, or pass "rail": "NEFT").pending_approval and are sent only after someone approves them in the dashboard.pending_approval → queued → processing → success | failed. A successful payout can later become reversed if the beneficiary bank returns it.GET /v1/payouts/{id}
GET /v1/payouts?reference_id=VENDOR-7781
POST /v1/payouts/bulk {"payouts": [ {...}, {...} ]} # up to 500, each row accepted or rejected on its own
POST /v1/vpa/validate {"vpa": "name@bank"}
GET /v1/balance
GET /v1/settlements
Add an HTTPS endpoint in Dashboard → Developers. We POST JSON like:
{
"id": "evt_…", "type": "payment.success", "mode": "test", "created_at": "2026-11-13T10:00:00.000Z",
"data": { "id": "pay_…", "object": "payment", "order_id": "ORD-1001", "amount": 49900, "status": "success", "utr": "431298765012", … }
}
Events: payment.success, payment.failed, payment.expired, refund.success, refund.failed, payout.success, payout.failed, payout.reversed, settlement.created, settlement.paid.
Header X-Paygit-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC_SHA256(secret, t + "." + raw_body). Compute it over the raw request bytes and reject timestamps older than 5 minutes.
// Node.js (Express): app.post('/webhooks/paygit', express.raw({type: 'application/json'}), handler)
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('=')));
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const want = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
return want.length === v1.length && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(v1));
}
id and treat the object's status as the truth.| Use this value | What happens |
|---|---|
| Checkout page buttons | Approve or decline a test payment by hand. |
payer_vpa: success@sim | Payment succeeds automatically after a couple of seconds. |
payer_vpa: fail@sim | Payment is declined automatically. |
Payout to fail@sim or an account ending 0000 | Payout fails and the hold is released. |
Payout to pending@sim | Payout stays processing until resolved in the operator's simulator page. |
| Any other beneficiary | Payout succeeds. |
| Code | Meaning |
|---|---|
unauthorized | Missing or wrong API key. |
validation_error | A field is missing or malformed; see details. |
duplicate_order_id / duplicate_reference_id | That reference already exists; details has the existing id. |
insufficient_balance | Available balance does not cover the payout plus fee, or the refund. |
amount_limit_exceeded, daily_limit_exceeded, rail_limit_exceeded, velocity_limit_exceeded | A risk limit was hit. |
blocked | The payer or beneficiary is on the blocklist. |
merchant_not_active, live_mode_unavailable | Live mode is not available for this account yet. |
bank_unavailable | The bank could not be reached. Retry the request. |
rate_limited | More than 1200 requests a minute on one key. |