Developers
Integrate once. Add gateways later.
A single REST API and one webhook contract sit in front of every provider you connect. Swapping or adding a gateway is a configuration change, not a sprint.
Quickstart
Your first payment in two calls
Create a payment with smart routing enabled, and OrcoPay picks the gateway, handles the retry and reports a single final status.
pip install requestsBefore you start
- 1Create an account and generate a sandbox key
- 2Connect at least one gateway in test mode
- 3Set a webhook endpoint to receive final statuses
import os
import requests
payment = requests.post(
"https://api.orcopay.com/v1/payments",
headers={
"Authorization": f"Bearer {os.environ['ORCOPAY_API_KEY']}",
"Idempotency-Key": "ord_9F42B7",
},
json={
"amount": 1840000, # minor units
"currency": "INR",
"method": "card",
"routing": "smart", # or a named rule set
"reference": "ord_9F42B7",
"customer": {
"id": "cus_71bd",
"email": "asha@example.com",
},
},
).json()
print(payment["status"]) # "authorized"
print(payment["gateway"]) # "razorpay"
print(payment["attempts"]) # 2API
Designed to be boring in production
The interesting behaviour belongs in the routing engine, not in your integration code.
Idempotent by default
Every write accepts an idempotency key. Replaying a request returns the original result instead of creating a second payment.
Provider-agnostic shapes
Requests and responses look the same regardless of which gateway handled the transaction. Raw provider payloads stay available.
Predictable errors
Stable machine-readable error codes, with the retry semantics documented per code rather than inferred from a message string.
Versioned, never broken
Breaking changes ship behind a dated API version. Your integration keeps working until you choose to move.
Checkout SDK
One frontend integration, every gateway
Your server creates the payment with your secret key. Your frontend calls one function with the resulting payment id — OrcoPay decides which gateway handles it and shows the right UI for it. Add a gateway later and every merchant using the SDK gets it automatically, with no frontend changes.
<script src="https://cdn.orcopay.com/orcopay.global.js"></script>npm install @orcopay/sdk// your server — never the browser. Uses your secret key.
const res = await fetch("https://api.orcopay.com/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ORCOPAY_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: 49900, // minor units
currency: "INR",
reference: "ord_9F42B7",
customer: { email: "asha@example.com" },
}),
});
const { id: paymentId, gateway } = await res.json();
// gateway is already decided here (e.g. "payu") — useful for your own
// order records. Hand paymentId to your frontend; the secret key and
// gateway-specific details never leave your server.<script src="https://cdn.orcopay.com/orcopay.global.js"></script>
<script>
Orcopay.checkout({
paymentId: "{{ paymentId }}", // from your server, above
onSuccess(result) {
window.location.href = `/orders/confirmed?ref=${result.reference}`;
},
onFailure(error) {
alert(error.message);
},
onClose() {
// customer dismissed the checkout overlay before paying
},
});
</script>Bundler-based apps
Same call, imported instead of global — nothing else about the flow changes.
import { checkout } from "@orcopay/sdk";
checkout({
paymentId,
onSuccess: (result) => confirmOrder(result.reference),
onFailure: (error) => showError(error.message),
onClose: () => resetCheckoutButton(),
});What the customer sees
checkout() shows a branded loading overlay, then hands off to whichever mode the selected gateway needs. Your integration code is identical no matter which one runs.
redirectFull-page handoff to the gateway's own hosted checkout page.
sdk_modalThe gateway's native checkout opens as an overlay — the customer never leaves your page.
upi_intentDeep-links into an installed UPI app on mobile, or shows a QR code on desktop.
hosted_fieldsCard fields render inline, styled to match your page, without card data ever touching your servers.
Callbacks
Three callbacks cover every outcome — nothing gateway-specific leaks into your handlers.
onSuccess(result)Payment completed. result.reference is your payment's id; result.mode is which of the modes above handled it.
onFailure(error)Initiation failed or the gateway declined. error.message is safe to show the customer.
onClose()The customer dismissed the checkout overlay before finishing — not itself a failure.
Your secret key never reaches the browser
Only your server calls OrcoPay with Authorization: Bearer. Your frontend only ever holds a paymentId, which is scoped to one fixed amount and can't be reused to create a new charge — safe to embed directly in the page.
Android SDK is on the roadmap
Not published yet. It'll follow the same contract as the Web SDK above — same paymentId flow, same onSuccess/onFailure/onClose shape — wrapping Android's native checkout instead of the browser DOM.
Get notifiediOS SDK is on the roadmap
Not published yet. It'll follow the same contract as the Web SDK above — same paymentId flow, same onSuccess/onFailure/onClose shape — wrapping iOS's native checkout instead of the browser DOM.
Get notifiedFlutter SDK is on the roadmap
Not published yet. It'll follow the same contract as the Web SDK above — same paymentId flow, same onSuccess/onFailure/onClose shape — wrapping Flutter's native checkout instead of the browser DOM.
Get notifiedReact Native SDK is on the roadmap
Not published yet. It'll follow the same contract as the Web SDK above — same paymentId flow, same onSuccess/onFailure/onClose shape — wrapping React Native's native checkout instead of the browser DOM.
Get notifiedWebhooks
One event schema, whatever the gateway
Every provider names things differently. OrcoPay normalizes them, signs the payload, and keeps retrying until you acknowledge it.
{
"id": "evt_4c81f0aa27",
"type": "payment.authorized",
"created": "2026-08-26T09:14:22Z",
"data": {
"payment": {
"id": "pay_8f2ad41c93",
"reference": "ord_9F42B7",
"amount": 1840000,
"currency": "INR",
"status": "authorized",
"gateway": "razorpay",
"attempts": [
{ "gateway": "payu", "status": "declined", "code": "issuer_timeout" },
{ "gateway": "razorpay", "status": "authorized", "code": null }
]
}
}
}import { verifyWebhook } from "@orcopay/node";
app.post("/webhooks/orcopay", express.raw({ type: "*/*" }), (req, res) => {
const event = verifyWebhook({
payload: req.body,
signature: req.header("OrcoPay-Signature"),
secret: process.env.ORCOPAY_WEBHOOK_SECRET,
toleranceSeconds: 300,
});
if (event.type === "payment.authorized") {
fulfil(event.data.payment.reference);
}
res.sendStatus(204); // ack fast; OrcoPay retries anything non-2xx
});Event types
- payment.authorized
- payment.captured
- payment.failed
- payment.refunded
- payout.settled
- dispute.opened
- dispute.resolved
- gateway.degraded
Server-side API clients
For calling the REST API from your backend — create payments, verify webhooks. Separate from the Checkout SDK above, which runs in the browser.
- Node.js
@orcopay/node - Python
orcopay - PHP
orcopay/orcopay-php - Go
github.com/orcopay/orcopay-go
Sandbox and status
The sandbox mirrors production routing, including the failure modes you need to handle.
- Simulate soft declines, hard declines and issuer timeouts
- Force a specific gateway to fail and watch the cascade
- Replay any webhook against a local tunnel
- Per-provider uptime and latency published continuously
Get started today
Start in the sandbox
Generate a test key, connect a simulated gateway, and put a payment through the full cascade in a few minutes.
- Easy onboarding
- Dedicated support
- API access
