@extends('layouts.base') @section('title', 'API Documentation') @php $base = url('/api/v1'); @endphp @push('head') @endpush @section('body')
@include('layouts.public-nav') {{-- Hero --}}
v1 · Stable

API Reference

Integrate UPI payments into your app in minutes. Create orders, redirect customers, and receive real-time webhooks.

Base URL {{ $base }}
{{-- Sidebar --}} {{-- Content --}}
{{-- Introduction --}}
Getting Started

Introduction

The {{ config('app.name') }} API lets you create UPI payment orders, redirect customers to a hosted checkout, and receive signed webhooks when payments complete. All API calls are server-to-server — never expose your secret in a browser or mobile app.

@foreach ([ ['🔒', 'Secure', 'HMAC-SHA256 signed webhooks. Credentials encrypted at rest.'], ['⚡', 'Real-time', 'Instant payment confirmation via polling or webhooks.'], ['🔄', 'Auto-rotation', 'Distribute load across multiple UPI accounts automatically.'], ] as [$icon, $title, $desc])
{{ $icon }}
{{ $title }}

{{ $desc }}

@endforeach
{{-- Authentication --}}

Authentication

Every request must include your API key and secret as headers. Find them in Dashboard → API & Webhooks.

⚠

Never expose your X-Api-Secret in client-side code, mobile apps, or public repositories.

@include('docs._code', ['title' => 'Required headers', 'lang' => 'http', 'code' => "X-Api-Key: key_xxxxxxxxxxxxxxxx X-Api-Secret: sec_xxxxxxxxxxxxxxxx Accept: application/json Content-Type: application/json"])
{{-- Create Order --}}
POST /api/v1/orders

Create order

Creates a pending order and returns a payment_url. Redirect your customer there. For standard UPI accounts, a upi_intent deep-link is also returned so you can render your own QR.

Request body
@foreach ([ ['amount', 'number', true, 'Amount in INR (₹1 – ₹1,00,000). Up to 2 decimal places.'], ['client_order_id', 'string', true, 'Your unique order ID. Allowed: A–Z, 0–9, _ and -. Max 64 chars.'], ['redirect_url', 'url', false, 'Customer is redirected here after payment with order_id, client_order_id and status as query params.'], ['customer_name', 'string', false, 'Shown on the checkout page.'], ['customer_email', 'email', false, 'Customer email address.'], ['customer_mobile', 'string', false, 'Customer mobile number.'], ['note', 'string', false, 'Shown in the customer\'s UPI app. Max 50 chars.'], ['payment_account_id', 'integer', false, 'Pin to a specific UPI account. Falls back to rotation if the account is full or inactive.'], ] as [$name, $type, $required, $desc]) @endforeach
Parameter Type Description
{{ $name }} @if ($required) required @else optional @endif {{ $type }} {{ $desc }}
@include('docs._code', ['title' => 'cURL', 'lang' => 'bash', 'code' => "curl -X POST {$base}/orders \\ -H \"X-Api-Key: key_xxx\" \\ -H \"X-Api-Secret: sec_xxx\" \\ -H \"Accept: application/json\" \\ -H \"Content-Type: application/json\" \\ -d '{\"amount\": 199, \"client_order_id\": \"INV1001\", \"redirect_url\": \"https://yoursite.com/thanks\"}'"]) @include('docs._code', ['title' => 'Response · 201 Created', 'lang' => 'json', 'code' => '{ "success": true, "data": { "order_id": "ORDAB12CD34EF56GH78", "client_order_id": "INV1001", "amount": "199.00", "currency": "INR", "status": "pending", "payment_url": "'.url('/pay/ORDAB12CD34EF56GH78').'", "upi_intent": "upi://pay?pa=merchant@upi&am=199.00&tr=ORDAB12CD34EF56GH78", "expires_at": "2026-01-01T12:15:00+05:30" } }'])
{{-- Get Status --}}
GET /api/v1/orders/{order_id | client_order_id}

Get order status

Returns the latest order state, checking the payment provider if the order is still pending. Always confirm with this endpoint or a verified webhook before fulfilling an order — never trust the redirect URL alone.

@include('docs._code', ['title' => 'cURL', 'lang' => 'bash', 'code' => "curl {$base}/orders/INV1001 \\ -H \"X-Api-Key: key_xxx\" \\ -H \"X-Api-Secret: sec_xxx\" \\ -H \"Accept: application/json\""]) @include('docs._code', ['title' => 'Response · 200 OK', 'lang' => 'json', 'code' => '{ "success": true, "data": { "order_id": "ORDAB12CD34EF56GH78", "client_order_id": "INV1001", "amount": "199.00", "currency": "INR", "status": "paid", "utr": "427123456789", "paid_at": "2026-01-01T12:10:00+05:30" } }'])
{{-- Order Object --}}

Order object

An order moves through these statuses:

@foreach ([ 'pending' => ['Waiting for the customer to pay. Expires after 15 minutes.', 'bg-amber-50 border-amber-200'], 'paid' => ['Payment confirmed. The utr field contains the bank reference.', 'bg-emerald-50 border-emerald-200'], 'expired' => ['Order was not paid before expires_at.', 'bg-slate-50 border-slate-200'], ] as $s => [$d, $cls])

{{ $d }}

@endforeach
{{-- List Accounts --}}
GET /api/v1/accounts

List UPI accounts

Returns your active UPI accounts with their IDs, daily limits, and remaining capacity. Use the id as payment_account_id when creating an order to pin it to a specific account.

@include('docs._code', ['title' => 'Response · 200 OK', 'lang' => 'json', 'code' => '{ "success": true, "rotation_mode": "random", "data": [ { "id": 3, "label": "Shop 1", "upi_id": "merchant@paytm", "provider": "paytm_business", "is_active": true, "daily_limit": "100000.00", "used_today": "42500.00", "remaining_today": "57500.00" } ] }'])
{{-- Rotation --}}

Rotation & daily limits

When you omit payment_account_id, the gateway automatically picks an account based on your rotation mode:

🎲
Random rotation (default)

A random account among those with remaining daily capacity.

⭐
Default account first

Your default account is used until it hits its daily limit, then falls back to others.

Each account has a daily limit (₹1,00,000 by default). Usage includes paid orders from today plus open pending orders. If no account has capacity, the API returns 422 with an amount error.

{{-- Webhooks --}}

Webhook events

Configure your endpoint in Dashboard → API & Webhooks. We send a signed POST request for each event. Respond with any 2xx status; failed deliveries are retried up to 6 times with exponential backoff.

Events
@foreach ([ ['order.paid', 'emerald', 'Payment confirmed. Safe to fulfil the order.'], ['order.expired', 'slate', 'Order was not paid before it expired.'], ] as [$event, $color, $desc]) @endforeach
{{ $event }} {{ $desc }}
ℹ

The same event may be delivered more than once. Always handle webhooks idempotently using client_order_id.

@include('docs._code', ['title' => 'Payload · order.paid', 'lang' => 'json', 'code' => '{ "event": "order.paid", "data": { "order_id": "ORDAB12CD34EF56GH78", "client_order_id": "INV1001", "amount": "199.00", "currency": "INR", "status": "paid", "utr": "4271234567890", "paid_at": "2026-01-01T12:10:00+05:30" } }']) @include('docs._code', ['title' => 'Payload · order.expired', 'lang' => 'json', 'code' => '{ "event": "order.expired", "data": { "order_id": "ORDAB12CD34EF56GH78", "client_order_id": "INV1001", "amount": "199.00", "currency": "INR", "status": "expired", "expires_at": "2026-01-01T12:15:00+05:30" } }'])
{{-- Verify Signature --}}

Verify signature

Every webhook includes an X-Signature header containing the HMAC-SHA256 of the raw request body, keyed with your API secret. Always verify this before processing.

@php $phpVerifyCode = implode("\n", [ ' 'PHP', 'lang' => 'php', 'code' => $phpVerifyCode]) @php $nodeCode = implode("\n", [ 'const crypto = require("crypto");', '', 'app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {', ' const expected = crypto', ' .createHmac("sha256", process.env.UPI_SECRET)', ' .update(req.body)', ' .digest("hex");', '', ' const given = req.get("X-Signature") || "";', ' if (given.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) {', ' return res.sendStatus(401);', ' }', '', ' const event = JSON.parse(req.body);', ' if (event.event === "order.paid") {', ' // Fulfil event.data.client_order_id (idempotently)', ' }', ' res.sendStatus(200);', '});', ]); @endphp @include('docs._code', ['title' => 'Node.js', 'lang' => 'javascript', 'code' => $nodeCode])
{{-- Errors --}}

Error codes

All errors return a JSON body with a message field. Validation errors (422) also include an errors object keyed by field name.

@foreach ([ ['401', 'red', 'Missing or invalid API key / secret.'], ['403', 'orange', 'Merchant account is disabled.'], ['404', 'slate', 'Order not found.'], ['422', 'amber', 'Validation failed — see the errors object for field-level details.'], ['429', 'purple', 'Rate limit exceeded (120 requests / minute).'], ['500', 'red', 'Unexpected server error. Contact support if this persists.'], ] as [$code, $color, $meaning]) @endforeach
Status Meaning
{{ $code }} {{ $meaning }}
{{-- SDKs / Code Examples --}}

Code examples

Quick-start snippets for common server-side languages.

@include('docs._code', ['title' => 'Python (requests)', 'lang' => 'python', 'code' => "import requests, hmac, hashlib headers = { 'X-Api-Key': 'key_xxx', 'X-Api-Secret': 'sec_xxx', 'Accept': 'application/json', 'Content-Type': 'application/json', } res = requests.post( '". $base ."/orders', json={'amount': 199, 'client_order_id': 'INV1001'}, headers=headers, timeout=10, ) data = res.json() payment_url = data['data']['payment_url']"]) @php $guzzleCode = implode("\n", [ ' "' . $base . '"]);', '', '$res = $client->post("/orders", [', ' "headers" => [', ' "X-Api-Key" => "key_xxx",', ' "X-Api-Secret" => "sec_xxx",', ' "Accept" => "application/json",', ' ],', ' "json" => ["amount" => 199, "client_order_id" => "INV1001"],', ']);', '', '$data = json_decode($res->getBody(), true);', '$paymentUrl = $data["data"]["payment_url"];', ]); @endphp @include('docs._code', ['title' => 'PHP (Guzzle)', 'lang' => 'php', 'code' => $guzzleCode])
@push('scripts') @endpush @endsection