Merchant API Reference
Version 1 · Base URL: https://payments.alvanlabs.com/api/v1
The Alvan Labs Pay API enables merchants to accept UPI payments with fully automatic reconciliation — no payment gateway required. When a customer initiates a payment, the API assigns a system-determined payment amount using proprietary reconciliation logic, enabling zero-touch matching of incoming credits to orders.
API calls only require your merchant_id. No OAuth tokens, no API keys in headers.
Payments are verified in real-time via instant email monitoring. Orders are typically verified within 1–3 seconds of payment — no manual checks needed.
Bank credit emails are parsed and matched to orders automatically — no manual reconciliation needed.
Getting Started
Everything you need to set up before going live
What you need to provide
Your UPI ID is the payment address where customers send money — e.g. yourname@okaxis, 9876543210@ybl.
✅ A personal UPI ID works perfectly. You do not need a dedicated merchant or business UPI ID. Any UPI ID linked to your bank account is supported.
The Gmail account that receives bank credit notifications when a UPI payment is credited to your account.
This must be the Gmail address registered with your bank for email alerts. Most banks send a credit email within 30–90 seconds of a payment.
A special read-only access token so our system can read your bank credit emails. This is not your Gmail login password — it is a separate 16-character app-specific password generated inside your Google account.
See the step-by-step guide below.
The exact email address your bank uses to send credit notification emails — e.g. alerts@hdfcbank.net, credit@axisbank.com.
This lets us filter only genuine bank credit emails from your inbox.
How to connect Gmail via OAuth2
Takes about 1 minute. Uses Google's official OAuth2 — no passwords shared.
Log in to your Merchant Dashboard
Click "Connect Gmail"
In the Gmail & Bank Alerts section, click the Connect Gmail → button. You will be redirected to Google's official sign-in page.
Authorize read-only access
Select the Gmail account that receives your bank credit alerts. Grant the read-only permission when prompted by Google.
Set your Bank Sender Email
Back in Settings, enter the email address your bank uses to send credit alerts (e.g. alerts@hdfcbank.net). This filters out all non-bank emails.
Done — instant monitoring is now active
Your dashboard will show ✅ Gmail API — Real-time Push Active. Payments will now be verified within seconds of the bank credit email arriving.
How to find your Bank Sender Email
Open your Gmail and search for a recent UPI credit notification from your bank. The "From" address is what you need. Common examples:
| Bank | Typical sender email |
|---|---|
| HDFC Bank | alerts@hdfcbank.net |
| ICICI Bank | info@icicibank.com |
| Axis Bank | noreply@axisbank.com |
| SBI | sbialerts@alerts.sbi.co.in |
| Kotak | alerts@kotak.com |
| IndusInd | alerts@indusind.com |
| Yes Bank | statements@yesbank.in |
If your bank is not listed, simply open any recent UPI credit email and check the "From" field.
Your Privacy & Security
- ✅ Email access is read-only. Gmail OAuth2 grants read-only access to your inbox. It cannot send, delete, or modify anything.
- ✅ OAuth2 — no password shared. We never see your Gmail password. Google issues a secure access token that can be revoked at any time from your Google Account.
- ✅ Only bank sender emails are read. Our parser filters strictly by the bank sender email you configure. Emails from any other sender are completely ignored.
- ✅ No email content is stored. We extract only the credited amount and UTR — no email body, subject, or other content is persisted.
- ✅ You can revoke access anytime. Disconnect from your dashboard Settings page, or remove access directly at myaccount.google.com/permissions. This immediately terminates our access.
- ✅ Credentials are never shared. Your details are isolated per merchant account and are never shared with other merchants or third parties.
Questions? Email us at payments@alvanlabs.com
Authentication
How to identify yourself as a merchant
All API requests are authenticated using your Merchant Key — a unique string in the format mch_<24 hex chars>. This key is passed as a request body field or query parameter depending on the endpoint. There are no bearer tokens or API key headers required.
Where to find your Merchant Key:
- Log in to your merchant dashboard at payments.alvanlabs.com/dashboard
- Go to Settings — your Merchant Key is shown there
- Click the copy icon (📋) next to the key
merchant_id = "mch_4b7d2e9f1a3c058762de4f91"
Error Handling
Standard HTTP status codes and error response format
All errors return a JSON body with a detail field describing the issue.
| HTTP Status | Meaning | Example detail |
|---|---|---|
| 400 | Bad Request — malformed JSON or missing field | value is not a valid string |
| 404 | Merchant not found (invalid merchant_id) | Merchant not found. |
| 429 | Rate limit exceeded — too many checkout requests from the same IP within 1 minute, or monthly plan order limit reached | Too many checkout requests. Please try again in a minute. |
| 503 | No payment sessions available — all 99 cent slots in use, try again shortly | All payment slots full. |
| 500 | Internal server error | Payment session creation failed. |
{
"detail": "Merchant not found."
}
End-to-End Payment Flow
How a payment goes from initiation to verified
Merchant calls /api/v1/initiate-pay
Your server sends order details + merchant_id. The API creates a payment session with a system-assigned amount and returns a UPI deep link.
Customer pays via UPI
Display the upi_link as a QR code or "Pay Now" button. The customer must pay the exact unique_amount for automatic matching.
Merchant polls /api/v1/check-payment/{order_id}
Poll every 5–10 seconds until status is paid or expired. Recommended timeout: 60 minutes.
Credit email detected instantly (~1–3 seconds)
Our system monitors your connected Gmail account in real-time. The moment the bank credit email arrives, it is matched by amount and UTR, the order is marked verified, and your next poll returns verified.
Post-payment action triggered (optional)
Depending on your integration type, the engine automatically: updates the WooCommerce order to processing via REST API, fires a Webhook POST to your configured URL, or sends a WhatsApp notification.
/api/v1/initiate-pay
Creates a new payment session. Returns a system-assigned unique_amount and UPI deep link for the customer to pay.
Request Body (JSON)
| Field | Type | Required | Description |
|---|---|---|---|
| order_id | string | required | Your platform's unique order identifier (e.g. WooCommerce order ID). Must be unique per merchant. Max recommended: 64 chars. |
| customer_email | string | required | Customer's email address. Stored for reference and timeout notification emails. |
| customer_phone | string | required | Customer's phone number. Stored for reference. |
| base_amount | float | required | Order amount in INR (e.g. 500.00). The API will compute and return a unique_amount for the customer to pay — always use that value, not this one. |
| merchant_id | string | required | Your merchant key (format: mch_...). Found in your merchant dashboard under Settings. |
Success Response 200 OK
| Field | Type | Description |
|---|---|---|
| status | string | Always "success" on 200. |
| order_id | string | Echoes back the order_id you sent. |
| unique_amount | float | Critical: The exact amount the customer must pay. Determined by proprietary reconciliation logic. Always display and charge this value — never the original base_amount. |
| upi_link | string | UPI Intent URL in the format upi://pay?pa=...&pn=...&am=...&cu=INR. Use this to render a QR code or "Pay Now" button on mobile. |
| created_at | string (ISO 8601) | Timestamp when the order was created (IST). |
| expires_at | string (ISO 8601) | Timestamp when the order expires (created_at + 1 hour). After expiry the order moves to failed_orders. |
Request
POST /api/v1/initiate-pay Content-Type: application/json { "order_id": "WC-1042", "customer_email": "john@example.com", "customer_phone": "9876543210", "base_amount": 500.00, "merchant_id": "mch_4b7d2e9f1a3c058762de4f91" }
Response
{ "status": "success", "order_id": "WC-1042", "unique_amount": 500.07, "upi_link": "upi://pay?pa=merchant@upi&pn=Shop&am=500.07&cu=INR", "created_at": "2026-05-14T18:30:00", "expires_at": "2026-05-14T19:30:00" }
/api/v1/check-status/{order_id}
Returns full order details including status, amount, and timestamps. Useful for debugging or displaying a detailed status page.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| order_id | string (path) | required | The order_id you provided when creating the order. |
| merchant_id | string (query) | required | Your merchant key. Scopes the lookup to your account. |
Response Fields
| Field | Type | Description |
|---|---|---|
| order_id | string | Your order identifier. |
| status | string | pending, verified, or expired. Note: this endpoint returns the internal status (verified, not paid). |
| amount | float | The unique amount assigned to this order. |
| created_at | string | ISO 8601 timestamp when order was created (IST). |
| expires_at | string | ISO 8601 timestamp when order expires (IST). |
Request
GET /api/v1/check-status/WC-1042?merchant_id=mch_4b7d2e9f1a3c058762de4f91
Response
{ "order_id": "WC-1042", "status": "verified", "amount": 500.07, "created_at": "2026-05-14T18:30:00", "expires_at": "2026-05-14T19:30:00" } // Not found response: { "error": "Order not found" }
/api/v1/check-payment/{order_id}
Use for polling
Lightweight polling endpoint. Returns a simple success flag and status field. This is the primary endpoint to poll while waiting for payment confirmation.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| order_id | string (path) | required | The order ID. |
| merchant_id | string (query) | required | Your merchant key. |
Response — All Possible States
| success | status | Meaning | Action |
|---|---|---|---|
| true | pending | Order created, awaiting payment | Continue polling |
| true | verified | Payment received and verified ✅ | Mark order complete |
| true | expired | Order not paid within 1 hour | Stop polling, show failure |
| false | not_found | Wrong order_id or merchant_id | Stop polling, check IDs |
Request
GET /api/v1/check-payment/WC-1042?merchant_id=mch_4b7d2e9f1a3c058762de4f91
Responses
// Still waiting: { "success": true, "status": "pending" } // Payment confirmed: { "success": true, "status": "verified" } // Not found: { "success": false, "status": "not_found" }
Order Statuses
All possible states an order can be in
Order created, waiting for payment. Active for up to 1 hour. The UPI link is valid and the payment session is active.
Payment received and matched to this order via Gmail parsing. Both /check-payment and /check-status return this as "verified".
Order was not paid within 1 hour and was moved to failed_orders. Can be manually verified from the merchant dashboard if payment was received late.
Only returned by /check-payment. Means the order_id + merchant_id combination doesn't exist in the database.
UPI Deep Link
How to use the upi_link returned by initiate-pay
The upi_link is a standard UPI Intent URL. You can use it in two ways:
1. QR Code (Desktop / Any device)
Convert the upi_link to a QR code using any JS library. The customer scans it with their UPI app.
// Using qrcode.js
QRCode.toCanvas(canvas, upiLink, { width: 200 })
// Or: qrcode-generator, jsQR, etc.
2. "Pay Now" Button (Mobile browsers)
On mobile devices, the UPI link opens the installed UPI app directly when set as an anchor href.
<a href="{{ upi_link }}" class="pay-btn">
Pay ₹{{ unique_amount }}
</a>
upi://pay?pa=merchant@upi&pn=MerchantName&am=500.07&cu=INR
pa= merchant UPI ID (payee address)pn= merchant name (payee name)am=unique_amount— always use this, not base_amountcu= currency, always INR
Polling Strategy
Best practices for polling /check-payment
Recommended intervals:
- Poll every 5 seconds for the first 2 minutes (most payments complete by then)
- Poll every 15 seconds from 2–10 minutes
- Poll every 30 seconds from 10–60 minutes
- Stop polling after 60 minutes — order will have expired
// JavaScript polling example async function pollPayment(orderId, merchantId) { const maxTime = 60 * 60 * 1000; // 1 hour const start = Date.now(); let interval = 5000; // start at 5s while (Date.now() - start < maxTime) { const res = await fetch( `/api/v1/check-payment/${orderId}?merchant_id=${merchantId}` ); const data = await res.json(); if (data.status === 'paid') { // Payment confirmed! handleSuccess(data); return; } if (!data.success) { handleError('Order not found'); return; } // Back off over time if (Date.now() - start > 10 * 60 * 1000) interval = 30000; else if (Date.now() - start > 2 * 60 * 1000) interval = 15000; await new Promise(r => setTimeout(r, interval)); } handleTimeout(); // 1 hour passed }
Proprietary Reconciliation
How Alvan Labs Pay matches payments to orders automatically
Alvan Labs Pay uses proprietary reconciliation technology to automatically match incoming UPI payments to the correct order — without any manual intervention. When you initiate a payment, the system assigns a unique_amount for that session. This amount must be paid exactly as specified.
- Always display and charge the
unique_amountreturned by the API — never the originalbase_amount. - The
unique_amountmay differ slightly frombase_amount. The difference, if any, is negligible and is by design. - If the maximum concurrent payment sessions are reached, the API returns
503. This resolves automatically as sessions complete or expire.
Order Expiry
What happens when an order is not paid within 1 hour
- ✅ Orders expire 1 hour after creation.
- ✅ An automated background job runs every minute to detect expired orders and move them to
failed_orders. - ✅ A timeout email is sent to the customer's email address when an order expires.
- ✅ Expired orders are visible in the merchant dashboard under the "Expired" filter.
- ✅ Merchants can manually verify an expired order from the dashboard if they received payment after expiry.
- ⚠️ After expiry, the payment session is released and new orders can be initiated for the same amount.
WooCommerce Integration
Built-in automatic order status sync
When your merchant account has integration_type = woo configured, the system automatically calls the WooCommerce REST API to update the order to processing when payment is verified. No additional code needed.
Required settings (configure in merchant dashboard → Settings):
| Setting | Value |
|---|---|
| woo_api_url | https://yourstore.com/wp-json/wc/v3 |
| woo_consumer_key | WooCommerce REST API Consumer Key (ck_...) |
| woo_consumer_secret | WooCommerce REST API Consumer Secret (cs_...) |
| integration_type | Must be set to woo |
order_id to /initiate-pay must be the numeric WooCommerce order ID. The system uses PUT /wp-json/wc/v3/orders/{order_id} to update the status.
Shopify Integration
Native Shopify Checkout Extension with Direct Deep-Linking
Accept zero-fee UPI payments directly in your Shopify Store using our dedicated Shopify App. The integration embeds a native Checkout UI Extension block directly on your store's **Thank You** and **Order Status** pages.
Our Shopify extension uses official sandboxed link hooks. On mobile devices (including Safari and Chrome on iOS), customers can simply tap an interactive button to instantly launch their local UPI app (GPay, PhonePe, Paytm, or BHIM) with pre-filled and locked payment amounts, boosting mobile conversion rates significantly.
- Zero Code Required: One-click merchant installation via your AlvanLabs Pay dashboard.
- Clear Price Breakdown: Separates the base item amount from verification charges transparently on screen.
- Real-time Fulfillment: As soon as the email parsing matches the credit bank notification, the Shopify order is automatically tagged and marked as
Paid.
Webhooks Coming Soon
Push notifications when payment is verified
When integration_type = webhook, the system will POST a JSON payload to your configured webhook_url when an order is verified. The webhook payload format will be:
// Planned webhook payload (subject to change) { "event": "payment.verified", "order_id": "WC-1042", "amount": 500.07, "merchant_id": "mch_...", "verified_at": "2026-05-14T18:32:11" }
Contact payments@alvanlabs.com to request early access.
Custom PHP Integration
Full example for non-WooCommerce PHP applications
<?php // ─── Step 1: Initiate payment ─────────────────────────────── $merchantId = 'mch_4b7d2e9f1a3c058762de4f91'; $orderId = 'ORDER-' . time(); // your unique order ID $payload = json_encode([ 'order_id' => $orderId, 'customer_email' => 'customer@example.com', 'customer_phone' => '9876543210', 'base_amount' => 500.00, 'merchant_id' => $merchantId, ]); $ch = curl_init('https://payments.alvanlabs.com/api/v1/initiate-pay'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 10, ]); $response = json_decode(curl_exec($ch), true); curl_close($ch); if (!$response || $response['status'] !== 'success') { die('Payment initiation failed: ' . ($response['detail'] ?? 'unknown error')); } $uniqueAmount = $response['unique_amount']; // e.g. 500.07 $upiLink = $response['upi_link']; // display as QR or button $expiresAt = $response['expires_at']; // show countdown timer // Store $orderId + $uniqueAmount in your DB for reference // ─── Step 2: Poll for payment status (server-side or AJAX) ─ function checkPaymentStatus($orderId, $merchantId): string { $url = "https://payments.alvanlabs.com/api/v1/check-payment/{$orderId}?merchant_id={$merchantId}"; $ch = curl_init($url); curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]); $data = json_decode(curl_exec($ch), true); curl_close($ch); return $data['status'] ?? 'error'; } // In your AJAX endpoint: $status = checkPaymentStatus($_GET['order_id'], $merchantId); echo json_encode(['status' => $status]); // "pending" | "paid" | "not_found" ?>
cURL Examples
Quick reference for testing all three endpoints
Initiate Payment
curl -X POST https://payments.alvanlabs.com/api/v1/initiate-pay \
-H "Content-Type: application/json" \
-d '{
"order_id": "TEST-001",
"customer_email": "test@example.com",
"customer_phone": "9876543210",
"base_amount": 500.00,
"merchant_id": "mch_4b7d2e9f1a3c058762de4f91"
}'
Check Payment (for polling)
curl "https://payments.alvanlabs.com/api/v1/check-payment/TEST-001?merchant_id=mch_4b7d2e9f1a3c058762de4f91"
Check Status (full details)
curl "https://payments.alvanlabs.com/api/v1/check-status/TEST-001?merchant_id=mch_4b7d2e9f1a3c058762de4f91"
Frequently Asked Questions
What happens if the customer pays the wrong amount?
pending until it expires. The payment will still reach the merchant's UPI account, but it won't be automatically matched to the order. The merchant can manually verify the order from the dashboard if needed.How long does verification take after payment?
Can I reuse the same order_id?
order_id must be unique per merchant. If you try to create an order with a duplicate order_id, you will get a database constraint error. Use a timestamp or UUID suffix if needed.What is the maximum number of concurrent pending orders?
503 — this resolves automatically as sessions complete or expire. For very high-volume use cases, contact support.Does the API work with all UPI apps?
upi://pay scheme is supported by all Android UPI apps. On iOS, QR code scanning is the recommended approach.Is HTTPS required?
https://payments.alvanlabs.com. HTTP requests will be redirected to HTTPS by nginx.How do I contact support?
Alvan Labs Pay · API v1 · payments@alvanlabs.com
© 2026 Alvan Labs. All rights reserved.