Skip to content
OrcoPay - Payment Orchestration Platform

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.

terminal
pip install requests

Before 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
create-payment.py
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"])  # 2

API

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 tag
<script src="https://cdn.orcopay.com/orcopay.global.js"></script>
npm
npm install @orcopay/sdk
1. your server
// 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.
2. your frontend
<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.

checkout.ts
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.

  • redirect

    Full-page handoff to the gateway's own hosted checkout page.

  • sdk_modal

    The gateway's native checkout opens as an overlay — the customer never leaves your page.

  • upi_intent

    Deep-links into an installed UPI app on mobile, or shows a QR code on desktop.

  • hosted_fields

    Card 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.

Webhooks

One event schema, whatever the gateway

Every provider names things differently. OrcoPay normalizes them, signs the payload, and keeps retrying until you acknowledge it.

payment.authorized.json
{
  "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 }
      ]
    }
  }
}
webhook-handler.ts
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
  • Pythonorcopay
  • PHPorcopay/orcopay-php
  • Gogithub.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