Everything you need to accept payments through Zoopay: create an order, redirect your customer to pay, and confirm the final status server-to-server. No login required to read this page — you'll need an API Key and Secret Key from your Merchant Dashboard to make live calls.
The Zoopay API is a RESTful JSON API served over HTTPS. Every request and response body is JSON. There are currently two endpoints: one to create a payment order and one to check its status.
Every request is authenticated with two credentials passed directly inside the JSON request body — an api_key and a secret_key. There is no separate auth header or token exchange step.
secret_key must never be embedded in client-side code (JavaScript, mobile apps) or committed to version control. Only ever call these endpoints from your own backend server.Initializes a payment and returns a checkout payment_url. Redirect your customer's browser to that URL to complete payment.
| Parameter | Type | Required | Description |
|---|---|---|---|
| api_key | string | Yes | Your public API key. |
| secret_key | string | Yes | Your secret key. |
| order_id | string | Yes | Your own unique order reference. |
| amount | decimal | Yes | Transaction amount in INR (e.g. 199.00). |
| redirect_url | string | Yes | Absolute URL the customer returns to after paying. |
| customer_name | string | No | Defaults to your account name if omitted. |
| customer_email | string | No | Defaults to your account email if omitted. |
| customer_phone | string | No | 10-digit mobile number. |
{
"api_key": "pk_live_xxxxxxxx",
"secret_key": "sk_live_xxxxxxxx",
"order_id": "SYS_ORDER_1001",
"amount": "199.00",
"customer_name": "John Doe",
"customer_email": "[email protected]",
"customer_phone": "9876543210",
"redirect_url": "https://yoursite.com/return"
}
{
"status": "success",
"message": "Order initialized successfully.",
"data": {
"transaction": {
"zoopay_txnid": "ZP260728A1B2C3D4",
"order_id": "SYS_ORDER_1001",
"status": "PENDING"
},
"financials": {
"currency": "INR",
"gross_amount": "199.00",
"platform_fee": "5.97",
"net_settlement_amount": "193.03"
},
"urls": {
"payment_url": "https://test.zoopay.site/pay?id=ZP260728A1B2C3D4"
}
}
}
Never rely solely on the customer returning to your redirect_url — they may close the tab before that happens. Always confirm the final outcome with this server-to-server call.
| Parameter | Required | Description |
|---|---|---|
| api_key | Yes | Your public API key. |
| secret_key | Yes | Your secret key. |
| order_id | Cond. | Required if zoopay_txnid is not passed. |
| zoopay_txnid | Cond. | Required if order_id is not passed. |
{
"api_key": "pk_live_xxxxxxxx",
"secret_key": "sk_live_xxxxxxxx",
"order_id": "SYS_ORDER_1001"
}
{
"status": "success",
"message": "Transaction details fetched successfully.",
"data": {
"transaction": {
"zoopay_txnid": "ZP260728A1B2C3D4",
"order_id": "SYS_ORDER_1001",
"gateway_txnid": "UPI20260101123456",
"payment_status": "SUCCESS"
},
"financials": {
"currency": "INR",
"gross_amount": "199.00",
"platform_fee": "5.97",
"net_settlement_amount": "193.03"
},
"customer": {
"name": "John Doe",
"email": "[email protected]",
"phone": "9876543210"
},
"timeline": {
"created_at": "2026-07-25 10:15:00",
"updated_at": "2026-07-25 10:16:32"
}
}
}
Programmatically create a payout from your wallet balance to a named recipient - modeled exactly like a "Send Payout to User" submission on your dashboard (not a self-withdrawal), since an API call always names who the money is going to. Every request lands in our staff review queue as PENDING; there is no instant/auto-transfer via this API - a member of our team always reviews and releases it manually.
reference_id is your own idempotency key. Retrying a call with the same reference_id for your account always returns the original request instead of creating a duplicate payout - always send one, and generate a new one only for a genuinely new payout.amount_type controls how amount is interpreted - the same two modes as the "I Know the Amount to Send" / "I Know the Amount to Receive" toggle on the dashboard. "send" means amount is the gross amount debited from your wallet; "receive" (the default for this API) means amount is the exact net amount the recipient should end up with, and we work out the gross for you.dest_type: "usdt", the final converted USDT amount (after payout charges) must currently be between 1,000 USDT and 10,000 USDT per request - a request outside this range returns a 400 error. These limits are set by the platform and can change.| Parameter | Type | Required | Description |
|---|---|---|---|
| api_key | string | Yes | Your public API key. |
| secret_key | string | Yes | Your secret key. |
| reference_id | string | Yes | Your own unique reference for this payout (max 100 chars) - used for idempotent retries and to look it up later. |
| recipient_name | string | Yes | Who this payout is being sent to (a customer, vendor, employee - or your own name/business if paying out to yourself). |
| amount | decimal | Yes | Interpreted according to amount_type below (e.g. 5000.00). |
| amount_type | string | No | "send" (gross, debited from wallet) or "receive" (net, what the recipient gets). Defaults to "receive". |
| dest_type | string | No | bank, upi, or usdt. Defaults to whichever destination you have configured (or provided below) if omitted. |
| bank_name / account_name / account_number / ifsc_code | string | No | Bank details for this payout only. Omit to use your saved default bank account instead. |
| upi_id | string | No | UPI ID for this payout only. Omit to use your saved default UPI ID instead. |
| usdt_address / usdt_network | string | No | USDT wallet for this payout only (only if USDT payouts are enabled on your account). Omit to use your saved default USDT wallet instead. |
A destination provided here is used only for this one request - it never changes your saved dashboard default.
{
"api_key": "pk_live_xxxxxxxx",
"secret_key": "sk_live_xxxxxxxx",
"reference_id": "PAYOUT_RUN_2026_08_01",
"recipient_name": "John Doe",
"amount_type": "receive",
"amount": "5000.00",
"dest_type": "bank",
"bank_name": "ICICI Bank",
"account_name": "John Doe",
"account_number": "000123456789",
"ifsc_code": "ICIC0001234"
}
{
"status": "success",
"message": "Payout request created successfully.",
"data": {
"payout": {
"payout_reference": "PYO260728A1B2C3D4",
"reference_id": "PAYOUT_RUN_2026_08_01",
"recipient_name": "John Doe",
"status": "PENDING"
},
"financials": {
"currency": "INR",
"gross_amount": "5050.51",
"platform_fee": "50.51",
"net_amount": "5000.00",
"usdt_amount": null,
"usdt_rate": null
},
"destination": {
"type": "BANK",
"masked_detail": "AC ****6789"
}
}
}
Look up a payout's current status by either identifier. Prefer Payout Webhooks for real-time updates and use this endpoint to poll only as a fallback or for reconciliation.
| Parameter | Required | Description |
|---|---|---|
| api_key | Yes | Your public API key. |
| secret_key | Yes | Your secret key. |
| payout_reference | Cond. | Required if reference_id is not passed. |
| reference_id | Cond. | Required if payout_reference is not passed. |
{
"api_key": "pk_live_xxxxxxxx",
"secret_key": "sk_live_xxxxxxxx",
"reference_id": "PAYOUT_RUN_2026_08_01"
}
{
"status": "success",
"message": "Payout details fetched successfully.",
"data": {
"payout": {
"payout_reference": "PYO260728A1B2C3D4",
"reference_id": "PAYOUT_RUN_2026_08_01",
"recipient_name": "John Doe",
"status": "COMPLETED",
"utr_number": "N2830200445",
"admin_remark": null
},
"financials": {
"currency": "INR",
"gross_amount": "5000.00",
"platform_fee": "50.00",
"net_amount": "4950.00"
},
"destination": {
"type": "BANK",
"masked_detail": "AC ****6789"
},
"timeline": {
"requested_at": "2026-08-01 09:00:00",
"processed_at": "2026-08-01 14:22:10"
}
}
}
status is PENDING, COMPLETED, or FAILED. admin_remark carries the rejection reason on a FAILED payout; processed_at is null until it's approved or rejected.
Both endpoints share the same error shape. Validation failures and authentication errors return HTTP 400; a maintenance window returns HTTP 503.
{
"status": "error",
"message": "Authentication Error: Invalid API credentials or account suspended."
}
{
"status": "error",
"message": "Service Unavailable: The payment engine is currently undergoing scheduled maintenance. Please try again later."
}
Zoopay pushes a real-time HTTP POST to your Callback URL the moment a transaction settles as SUCCESS or FAILED, so your system stays in sync even if the customer closes the browser before redirecting.
| Event | Fired When |
|---|---|
| payment.success | The transaction completes successfully. |
| payment.failed | The transaction is declined, fails, or times out. |
| payout.completed | Our team approves a payout request (see Create Payout) and releases the funds. |
| payout.failed | Our team rejects a payout request - admin_remark in the payload carries the reason. |
{
"event": "payment.success",
"zoopay_txnid": "ZP260728A1B2C3D4",
"order_id": "SYS_ORDER_1001",
"status": "success",
"amount": "199.00",
"net_amount": "193.03",
"currency": "INR",
"utr_number": "N2830200445",
"customer_name": "John Doe",
"customer_email": "[email protected]",
"customer_phone": "9876543210",
"created_at": "2026-07-25 10:15:00",
"event_time": "2026-07-25T10:16:32+05:30"
}
status is "failed" and utr_number/net_amount are typically null on a payment.failed event. order_id is your own reference from Create Order; zoopay_txnid is ours.
{
"event": "payout.completed",
"payout_reference": "PYO260728A1B2C3D4",
"reference_id": "PAYOUT_RUN_2026_08_01",
"settlement_id": 4821,
"recipient_name": "John Doe",
"status": "completed",
"amount": "5000.00",
"net_amount": "4950.00",
"currency": "INR",
"utr_number": "N2830200445",
"admin_remark": null,
"created_at": "2026-08-01 09:00:00",
"event_time": "2026-08-01T14:22:10+05:30"
}
On payout.failed, status is "failed", utr_number/net_amount are null, and admin_remark carries the rejection reason. reference_id and payout_reference are only populated for payouts created via the Create Payout API - both are null for a withdrawal submitted from the merchant's dashboard.
Every request carries an X-ZooPay-Signature header — an HMAC-SHA256 hash of the raw request body, signed with your secret_key. Verify it before trusting the payload, so a request sent to a guessed or leaked webhook URL can't be spoofed. Once verified, respond first, then do slow work — don't make ZooPay (or a retry) wait on your emails/fulfillment logic.
$payload = file_get_contents('php://input'); $header = $_SERVER['HTTP_X_ZOOPAY_SIGNATURE'] ?? ''; // "sha256=<hex>" $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret_key); if (!hash_equals($expected, $header)) { http_response_code(401); exit; // unverified - reject, do not process } $data = json_decode($payload, true); // Look up the order in your own system, e.g. by $data['order_id'], and update its status // based on $data['status'] ('success' or 'failed'). Keep this part fast - a slow DB write // here delays the response below, and a response that's too slow looks like a failed // delivery and gets retried. // This is the answer to "how do I return for the webhook": respond with a plain 2xx. // ZooPay only checks the status code, not the body - "OK" is enough, and nothing more // is required. Skip this and ZooPay sees it as failed and retries with backoff (see // Retries below), which would notify you again for the same event. http_response_code(200); echo 'OK'; // Do slow work (emails, fulfillment, third-party API calls) AFTER responding, not before - // e.g. queue a background job here rather than running it inline.
If your endpoint doesn't respond with an HTTP 2xx within 10 seconds, delivery is retried with exponential backoff — roughly 1 min, 5 min, 15 min, 30 min, 1 hr, 3 hr, 6 hr, then 12 hr — up to 8 attempts spanning about 24 hours, before being marked failed. Deliveries are independent per event, so a slow or briefly-down endpoint never blocks other notifications.
X-ZooPay-Signature header before trusting the payload (see above).order_id or zoopay_txnid.200 OK quickly. Do any slow processing (emails, fulfillment) after responding, not before — a slow handler risks a retry sending you a duplicate notification for the same event.