Payments API
Accept UPI, cards and net banking on your website. PayPe handles the gateway integration, status reconciliation and refunds; you make three API calls.
Sandbox https://api.paype.co.in
Production https://api.paype.co.in — same host, keyed by your credentials
What you need before starting
- A PayPe merchant account — you receive a MER… merchant ID
- Your website domain, registered with us (sent to the gateway on every charge)
- A server that can make HTTPS requests and receive them
Two ways to integrate
Hosted checkout is a single link. No server code, no keys — send the customer to a PayPe page and we return them to yours. Start here.
Server APIs give you control over the amount, order ID and redirect target, and let you query status and issue refunds from your own backend.
Payment lifecycle
Every payment holds exactly one state. Two of them are final; one is not.
State transitions
COMPLETED and FAILED are terminal — they never change again. PENDING is not a result. A customer who closed their browser mid-payment leaves an order in PENDING, and it may still succeed at the bank. Never mark an order paid, and never ship goods, on anything but a terminal state.
Do not treat the browser redirect back to your site as confirmation. A customer can close the tab, lose signal, or press back. Confirm from the Order status endpoint or a webhook — those are the only sources of truth.
Quickstart
Take a test payment in about two minutes.
1. Send the customer to checkout
Replace the merchant ID with your own. This link works today, with no keys required.
https://api.paype.co.in/?merchant=MER1783999043172
2. Customer pays
PayPe shows the payment page, the customer chooses UPI, card or net banking, and we return them to your result page with the order ID appended.
3. Confirm the result from your server
curl https://api.paype.co.in/api/order-status/PAYPE1783999272545
{
"state": "COMPLETED",
"amount": 1000,
"orderId": "OMO2607140912466357046693"
}
Read state at the root of the response. Nothing else decides whether the customer paid.
Amounts & identifiers
Amounts are in paise
Every amount in every request and response is an integer in paise. Sending 100 charges one rupee, not one hundred. Rounding errors here become refund disputes later, so convert once, in one place in your code.
amount = Math.round(rupees × 100)
Order IDs
You may supply your own merchantOrderId, or let PayPe generate one. Either way it must be unique for every attempt, under 63 characters, and contain only letters, numbers, hyphens and underscores.
| Identifier | Issued by | Example |
|---|---|---|
| merchantOrderId | You or PayPe | PAYPE1783999272545 |
| orderId | Gateway | OMO2607140912466357046693 |
| merchantRefundId | PayPe | REFUND1783997069495 |
| utr / arn | Bank | 455226570184 |
Keep the bank reference — UTR for UPI, ARN or BRN for cards and net banking. It is what a bank asks for when a customer disputes a charge.
API keys Spec
Server-to-server calls will be authenticated with a key pair issued per merchant from your dashboard: a key ID you may expose, and a secret you must not.
X-PayPe-Key-Id: pk_live_8f3ef32ad3fc X-PayPe-Signature: HMAC-SHA256(body, secret) Content-Type: application/json
Keeping the secret secret
- Server side only. Never in browser JavaScript, mobile app binaries, or a repository.
- Store it in your hosting platform's environment settings, not in a file you commit.
- Rotate it from the dashboard if it is ever exposed. Old keys stop working immediately.
Per-merchant keys are not implemented yet. Today the server APIs accept a
merchantId with no signature, which is safe only while the endpoints are
used by PayPe's own pages. Issue keys and verify the signature before any external
merchant is given server API access.
Hosted checkout Live
The fastest integration, and the one with the least to get wrong. Link or redirect the customer to your PayPe checkout URL.
<a href="https://api.paype.co.in/?merchant=MER1783999043172">
Pay with PayPe
</a>
| Query parameter | Description |
|---|---|
| merchant required | Your merchant ID. Determines which account the payment settles to, and which domain is declared to the gateway. |
If your account is suspended, checkout refuses the payment before the customer reaches the gateway. An unknown merchant ID is rejected the same way.
Create payment Live
Creates an order and returns the gateway URL to send the customer to. The order is recorded as PENDING and tracked until it reaches a terminal state.
Body
| Field | Type | Description |
|---|---|---|
| amount required | number | Amount in rupees. Converted to paise server side. |
| merchantId | string | Your MER… ID. Omit only for PayPe's own test page. |
curl -X POST https://api.paype.co.in/api/create-payment \ -H 'Content-Type: application/json' \ -d '{"amount": 149.50, "merchantId": "MER1783999043172"}'
{
"merchantOrderId": "PAYPE1783999272545",
"redirectUrl": "https://mercury.phonepe.com/transact/…"
}
Send the customer to redirectUrl. Store merchantOrderId against your own order record — you need it to check status and to refund.
Order status Live
Returns the current state of an order. Safe to call as often as you need; this is the endpoint to trust when deciding whether a customer has paid.
{
"state": "COMPLETED",
"amount": 14950,
"orderId": "OMO2607140912466357046693",
"paymentDetails": [
{
"paymentMode": "UPI_QR",
"state": "COMPLETED",
"rail": { "utr": "455226570184" }
}
]
}
Use the top-level state. The entries inside
paymentDetails describe individual attempts — a customer may fail once
on a card and then succeed on UPI, and only the root state accounts for that.
Treat unknown fields as normal. New payment methods add new keys, and a parser that rejects anything unexpected will break on the day we add one.
Refund Live
Refunds all or part of a completed payment. Partial refunds may be issued repeatedly until the original amount is exhausted.
Body
| Field | Type | Description |
|---|---|---|
| merchantOrderId required | string | The order being refunded. |
| amount required | number | Amount in rupees, up to the refundable balance. |
curl -X POST https://api.paype.co.in/api/refund \ -H 'Content-Type: application/json' \ -d '{"merchantOrderId": "PAYPE1783999272545", "amount": 50}'
{
"merchantRefundId": "REFUND1783997069495",
"amount": 5000,
"state": "PENDING"
}
What gets checked first
Three conditions are verified against PayPe's own records before any money moves:
| Condition | If it fails |
|---|---|
| The order exists | 404 Order not found in our records |
| The order is COMPLETED | 400 Only COMPLETED orders can be refunded |
| Refunds so far + this one ≤ original | 400 Refund exceeds refundable balance |
The balance error returns the arithmetic — original amount, already refunded, and remaining — so you can show the customer an accurate figure rather than a generic failure.
Refund status Live
Refunds start PENDING and usually settle within seconds, though bank processing can take longer. Poll this endpoint, or wait for the refund webhook.
{
"state": "COMPLETED",
"amount": 5000,
"originalMerchantOrderId": "PAYPE1783999272545",
"splitInstruments": [
{ "rail": { "utr": "455226570184" } }
]
}
Give the customer the UTR or ARN once the refund completes. It answers the "where is my money" question better than a date ever does.
Webhooks Spec
Rather than polling, register an HTTPS endpoint and PayPe will notify you when a payment or refund reaches a terminal state.
| Event | Sent when |
|---|---|
| payment.completed | A payment succeeded |
| payment.failed | A payment failed or expired |
| refund.completed | A refund reached the customer |
| refund.failed | A refund could not be processed |
{
"event": "payment.completed",
"payload": {
"merchantOrderId": "PAYPE1783999272545",
"state": "COMPLETED",
"amount": 14950
}
}
Verify every webhook before trusting it
Anyone can send a POST to your endpoint. Compute the expected signature from your webhook secret and compare — reject with 401 if it does not match.
// req.rawBody is the unparsed request body const expected = crypto .createHmac("sha256", process.env.PAYPE_WEBHOOK_SECRET) .update(req.rawBody) .digest("hex"); if (req.headers["x-paype-signature"] !== expected) { return res.status(401).json({ error: "unauthorized" }); } // Verified. Use payload.state, and reply 200 quickly. res.status(200).json({ received: true });
Rules for your handler
- Reply 200 fast. Do slow work afterwards. Timeouts trigger retries.
- Expect duplicates. The same event may arrive twice. Check whether you have already processed that order before acting on it.
- Read the root state, and identify the event from event.
- Webhooks are not a guarantee. Networks fail. Keep Order status as your fallback.
Merchant-facing webhooks are not built yet. PayPe currently receives webhooks from the gateway but does not forward events to merchants. Needed: a webhook URL and secret per merchant, a signed delivery, and a retry schedule.
Reconciliation Live
PayPe chases every PENDING order on your behalf until it resolves, so an abandoned checkout still ends up with a correct final state in your dashboard.
Polling schedule per pending order
Polling stops the moment a terminal state arrives, whether from the status API or a webhook. Nothing is polled after it is settled.
You do not need to build this yourself. It runs inside PayPe.
Errors
Errors return a JSON body with an error field describing what happened and, where useful, the values involved.
| Status | Meaning | What to do |
|---|---|---|
| 400 | Invalid request — bad amount, missing field, refund over balance | Fix the request. Do not retry unchanged. |
| 401 | Signature missing or wrong | Check the key and how you sign the body. |
| 403 | Merchant suspended | Contact PayPe. Payments are blocked until reactivated. |
| 404 | Unknown merchant, order or refund | Check the identifier. |
| 502 | Gateway rejected or was unreachable | Retry with the same order ID, then check status. |
| 503 | PayPe could not verify records — refunds are blocked rather than risked | Retry shortly. |
If PayPe cannot read the refund history for an order, it cannot know how much is left to refund — so it refuses instead of guessing. A failed refund is recoverable; an over-refund is money gone.
Test in sandbox
Sandbox behaves like production but moves no real money. Use it to exercise every path before you go live — especially the ones you hope never happen.
Cases to cover
| Scenario | Expected result |
|---|---|
| Successful UPI payment | Order reaches COMPLETED |
| Failed card payment | Order reaches FAILED, nothing shipped |
| Customer closes the tab mid-payment | Order stays PENDING, then resolves by reconciliation |
| Full refund | Refund COMPLETED, balance now zero |
| Partial refund, twice | Both succeed while the total stays within the original |
| Refund more than the balance | Rejected with the arithmetic returned |
| Duplicate webhook | Your handler processes the order once |
Test merchant IDs and keys are issued from your PayPe dashboard. Never put live credentials in documentation, support tickets, or chat messages.
Going live
- Confirm your domain is registered against your merchant account.
- Swap sandbox keys for live keys in your server environment.
- Point your webhook URL at your production endpoint and verify signatures there.
- Run one real payment of ₹1 and refund it, end to end.
Roadmap
Documented here so you can plan. These are not available yet — do not build against them.
Payouts
Send money to a bank account or UPI ID. Needed for marketplace sellers, refunds outside the original payment, and vendor disbursement.
Virtual accounts
A dedicated account number or UPI ID per customer, so incoming transfers reconcile automatically against an invoice.
Static QR
A printable QR for counters and storefronts, with the collection appearing in your dashboard.
Settlement reports
Daily settlement files showing gross collection, fees, refunds and the net amount credited to your bank.