Skip to main content

Custom Checkout Integration

Build your own checkout page using ZenPays APIs. Instead of redirecting customers to the ZenPays hosted checkout, you can collect payment details on your own page and confirm the payment via API.

Flow Overview

1. Your Server          2. Your Checkout Page        3. Your Checkout Page        4. Customer Browser
Create Intent ──► Fetch Intent Details ──► Confirm Payment ──► Redirect to TSP
(server-side) (client-side) (client-side) (payment page)

Step-by-step:

  1. Create Payment Intent (server-side) — Your backend creates a payment intent via API with the amount, currency, and customer info.
  2. Fetch Payment Intent (client-side) — Your checkout page fetches the intent details to get supported payment methods, countries, and channel limits.
  3. Confirm Payment (client-side) — After the customer selects a payment method and fills in details, your page sends a confirm request.
  4. Redirect — The confirm response returns a redirectUrl. Redirect the customer there to complete payment with the TSP (payment provider).
  5. Webhook — ZenPays sends a webhook to your server when the payment succeeds or fails.

Base URL

https://api.zenpayz.com/payment/api/v1

1. Create Payment Intent

Creates a payment intent on your server. This should be called from your backend (not the browser) since it requires your Merchant ID.

Request

POST /payment/api/v1/payment-intent

Headers:

HeaderRequiredDescription
X-Merchant-IDYesYour merchant ID
Content-TypeYesapplication/json
X-Request-IDNoUnique request ID for idempotency

Body:

FieldTypeRequiredDescription
amountnumberYesPayment amount (minimum: 0.01)
currencystringYesCurrency code (e.g. INR, USD, BRL)
customerEmailstringYes*Customer email (*required if no customerId)
customerPhonestringNoCustomer phone number
customerFirstNamestringNoCustomer first name
customerLastNamestringNoCustomer last name
customerCountrystringNoISO 3166-1 alpha-2 country code (e.g. IN)
customerIdstringNoExisting customer ID (skip email if provided)
paymentMethodstringNoPreferred payment method
descriptionstringNoPayment description (max 500 chars)
returnUrlstringNoURL to redirect customer after payment
metadataobjectNoCustom key-value data
requestIdstringNoUnique request ID (10-64 chars)

Example Request

{
"amount": 500,
"currency": "INR",
"customerEmail": "customer@example.com",
"customerPhone": "+911234567890",
"customerFirstName": "John",
"customerLastName": "Doe",
"customerCountry": "IN",
"description": "Order #12345",
"returnUrl": "https://yoursite.com/payment-complete",
"metadata": {
"orderId": "order_12345"
}
}

Example Response

{
"success": true,
"data": {
"intentId": "pi_1775052514718_yn6v3nc47",
"merchantId": "ZP_FIN_1774592804_0781001264",
"amount": 500,
"currency": "INR",
"status": "requires_payment_method",
"description": "Order #12345",
"expiresAt": "2026-04-01T11:00:04.296Z",
"processingFee": 0,
"createdAt": "2026-04-01T10:50:04.299Z",
"paymentPageUrl": "https://pay.zenpays.com/pay/pi_1775052514718_yn6v3nc47?token=pi_1775052514718_secret_abc123"
},
"message": "Payment intent created successfully",
"error": null,
"meta": {
"request_id": "req_1775052514718_abc123",
"timestamp": "2026-04-01T10:50:04.500Z",
"processing_time_ms": 200,
"api_version": "v1",
"endpoint": "POST /payment-intent"
}
}

Save the intentId — you'll need it for the next steps.


2. Fetch Payment Intent (Public)

Fetch the payment intent details on your checkout page. This is a public endpoint — no authentication required. Use this to render supported payment methods and validate amounts.

Request

GET /payment/api/v1/payment-intent/public/{intentId}

No headers required.

Example Response

{
"success": true,
"data": {
"intentId": "pi_1775052514718_yn6v3nc47",
"merchantId": "ZP_FIN_1774592804_0781001264",
"merchantName": "Your Store",
"merchantLogo": "https://cdn.example.com/logo.png",
"amount": "500.00000000",
"currency": "INR",
"description": "Order #12345",
"status": "requires_payment_method",
"expiresAt": "2026-04-01T11:00:04.296Z",
"isExpired": false,
"timeRemaining": 598,
"customerEmail": "customer@example.com",
"customerPhone": "+911234567890",
"supportedCountries": ["IN", "BR", "HK"],
"supportedPaymentMethods": ["UPI", "UPIQR-H5", "BANK_TRANSFER"],
"channelLimits": {
"INR": {
"UPI": { "min": 100, "max": 50000 },
"UPIQR-H5": { "min": 500, "max": 50000 }
}
},
"selectedTsp": "sulifu_pay",
"processingFee": 0,
"createdAt": "2026-04-01T10:50:04.299Z"
},
"message": "Payment intent details retrieved successfully",
"error": null,
"meta": {
"request_id": "req_1775052515000_xyz789",
"timestamp": "2026-04-01T10:50:05.000Z",
"processing_time_ms": 372,
"api_version": "v1",
"endpoint": "GET /payment-intent/public/pi_1775052514718_yn6v3nc47",
"user_type": "merchant"
}
}

Key Fields for Your Checkout UI

FieldHow to Use
supportedPaymentMethodsRender only these payment methods as options. Values like "UPI", "UPIQR-H5", "BANK_TRANSFER", "CARD", "PIX".
channelLimitsValidate the payment amount against min/max per method. If the amount is outside the range, disable or hide that method.
isExpired / timeRemainingIf isExpired is true, show an error — the intent can no longer be paid. timeRemaining is in seconds.
statusMust be "requires_payment_method" to proceed. Any other status means the intent is already processing or completed.
amount / currencyDisplay the payment amount to the customer.
merchantName / merchantLogoDisplay merchant branding on your checkout page.

Edge Cases to Handle

  • Expired intent: If isExpired is true, show "Payment link expired" and don't allow confirmation.
  • Already paid: If status is "succeeded" or "processing", show the appropriate message.
  • Amount below channel limit: If channelLimits.INR.UPI.min is 100 and the amount is 50, don't show UPI as an option.
  • UPI methods only for INR: UPI and UPIQR-H5 are only valid when currency is INR. Don't show them for other currencies.
  • No supported methods: If supportedPaymentMethods is empty or none pass the channel limits filter, show an error.

3. Confirm Payment Intent

After the customer selects a payment method and fills in details, confirm the payment. This is a public endpoint.

Request

POST /payment/api/v1/payment-intent/{intentId}/confirm

Headers:

HeaderRequiredDescription
Content-TypeYesapplication/json

Request Body Structure

{
"customerDetails": {
"name": "Customer Name",
"email": "customer@example.com",
"phone": "+911234567890",
"address": {
"country": "IN"
}
},
"paymentMethodDetails": {
"paymentMethod": "FIAT",
"paymentType": "UPI"
},
"deviceInfo": {
"userAgent": "Mozilla/5.0 ...",
"browserInfo": "Mozilla/5.0 ...",
"screenWidth": 1512,
"screenHeight": 982,
"deviceFingerprint": "unique_fingerprint_hash",
"metadata": {
"selectedPaymentMethod": "fiat",
"selectedMethod": "upi",
"specificPaymentMethod": "UPI",
"customerCurrency": "INR"
}
},
"confirmSource": "MERCHANT_SITE"
}

Field Reference

customerDetails (required):

FieldTypeRequiredDescription
namestringYesFull customer name
emailstringYesCustomer email
phonestringNoCustomer phone
address.countrystringNoISO country code

paymentMethodDetails (required):

FieldTypeDescription
paymentMethodstringAlways "FIAT" for fiat payments
paymentTypestringThe payment type (see table below)

confirmSource (required for custom checkout):

Always set to "MERCHANT_SITE". The value "ZENPAY_CHECKOUT" is reserved for the ZenPays hosted checkout page.


Payment Method Examples

UPI Payment

{
"paymentMethodDetails": {
"paymentMethod": "FIAT",
"paymentType": "UPI",
"upiId": ""
}
}

The upiId field can be empty — the TSP payment page will collect it.

UPIQR-H5 Payment (QR Code)

{
"paymentMethodDetails": {
"paymentMethod": "FIAT",
"paymentType": "UPIQR-H5"
}
}

No additional fields needed. The TSP generates the QR code on their payment page.

Bank Transfer

{
"paymentMethodDetails": {
"paymentMethod": "FIAT",
"paymentType": "BANK_TRANSFER",
"bankCode": "dp_hdfcbank_in"
}
}

Card Payment

{
"paymentMethodDetails": {
"paymentMethod": "FIAT",
"paymentType": "CARD",
"cardNumber": "4111111111111111",
"expiryMonth": "12",
"expiryYear": "2027",
"cvv": "123",
"cardHolderName": "John Doe"
}
}

PIX (Brazil)

{
"paymentMethodDetails": {
"paymentMethod": "FIAT",
"paymentType": "PIX"
}
}

Payment Type Reference

paymentTypeCurrencyExtra Fields
UPIINRupiId (optional)
UPIQR-H5INRNone
BANK_TRANSFERINRbankCode (optional)
CARDAnycardNumber, expiryMonth, expiryYear, cvv, cardHolderName
PIXBRLNone
NETBANKINGINRbankCode (optional)
WALLETINRwalletProvider (optional)

Confirm Response

Example (UPI)

{
"success": true,
"data": {
"intentId": "pi_1775052514718_yn6v3nc47",
"redirectUrl": "https://cashier.ppco.lol?orderNo=C2026040119435142093857",
"tspProvider": "sulifu_pay",
"externalTransactionId": "zpd6a82e78cedc1775052830530",
"status": "created",
"message": "Payment confirmed successfully. Redirecting to TSP.",
"requiresCardDetails": false,
"supportedCurrencies": ["INR"],
"supportedCountries": ["IN", "BR", "HK"],
"transactionId": "txn_1775052514718_1_mng4mhqh_p9axu5"
},
"message": "Payment confirmation successful",
"error": null,
"meta": {
"request_id": "req_1775052830084_b0c0htr5e",
"timestamp": "2026-04-01T14:13:51.654Z",
"processing_time_ms": 1569,
"api_version": "v1",
"endpoint": "POST /payment-intent/pi_1775052514718_yn6v3nc47/confirm",
"user_type": "merchant"
}
}

What to Do with the Response

  1. Check success — If false, show the error message to the customer.
  2. Redirect — If redirectUrl is present, redirect the customer's browser to that URL. This is the TSP's payment page where the customer completes the payment (enters UPI PIN, scans QR code, etc.).
  3. Save transactionId — Store this for tracking and reconciliation.
const response = await fetch(`/payment/api/v1/payment-intent/${intentId}/confirm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(confirmPayload),
});

const result = await response.json();

if (result.success && result.data.redirectUrl) {
// Redirect customer to TSP payment page
window.location.href = result.data.redirectUrl;
} else {
// Show error
alert(result.data?.message || result.error?.message || 'Payment failed');
}

Edge Cases and Error Handling

Payment Intent Errors

ScenarioHow to DetectWhat to Show
Intent expiredisExpired: true from GET response"This payment link has expired. Please request a new one."
Intent already paidstatus: "succeeded""This payment has already been completed."
Intent processingstatus: "processing""This payment is currently being processed."
Intent not found404 from GET endpoint"Payment not found."

Confirm Errors

ScenarioHow to DetectWhat to Show
Amount below minimumError response with min amount message"Minimum amount for UPI is 100 INR."
Amount above maximumError response with max amount message"Maximum amount for UPI is 50,000 INR."
Invalid payment method400 error"Selected payment method is not supported."
TSP unavailable500 error with routing failure"Payment service temporarily unavailable. Please try again."

Channel Limits Validation

Before showing payment methods to the customer, filter out methods where the amount is outside the channel limits:

const intentData = await fetchPaymentIntent(intentId);
const { amount, currency, supportedPaymentMethods, channelLimits } = intentData;

const currencyLimits = channelLimits?.[currency.toUpperCase()];

const availableMethods = supportedPaymentMethods.filter(method => {
if (!currencyLimits) return true;
const limit = currencyLimits[method];
if (!limit) return true;
return amount >= limit.min && amount <= limit.max;
});

// Render only `availableMethods` as options

UPI is INR-Only

UPI and UPIQR-H5 payment methods are only valid when the currency is INR. Don't show them for other currencies:

const UPI_METHODS = ['UPI', 'UPIQR-H5'];
const isINR = currency.toUpperCase() === 'INR';

const filteredMethods = availableMethods.filter(method => {
if (UPI_METHODS.includes(method) && !isINR) return false;
return true;
});

Webhooks

After the customer completes payment on the TSP page, ZenPays sends a webhook to your configured endpoint with the payment result.

See the Webhooks Guide for setup instructions and payload format.


Full Integration Example

// 1. Server-side: Create payment intent
const createResponse = await fetch('https://api.zenpayz.com/payment/api/v1/payment-intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Merchant-ID': 'your_merchant_id',
},
body: JSON.stringify({
amount: 500,
currency: 'INR',
customerEmail: 'customer@example.com',
returnUrl: 'https://yoursite.com/payment-complete',
}),
});
const { data: { intentId } } = await createResponse.json();

// 2. Client-side: Fetch intent details
const intentResponse = await fetch(
`https://api.zenpayz.com/payment/api/v1/payment-intent/public/${intentId}`
);
const { data: intent } = await intentResponse.json();

if (intent.isExpired) {
showError('Payment link expired');
return;
}

// 3. Client-side: Render payment methods filtered by channel limits
const methods = filterMethodsByLimits(
intent.supportedPaymentMethods,
intent.channelLimits,
intent.currency,
intent.amount
);

// 4. Client-side: After customer selects UPI and submits
const confirmResponse = await fetch(
`https://api.zenpayz.com/payment/api/v1/payment-intent/${intentId}/confirm`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customerDetails: {
name: 'John Doe',
email: 'customer@example.com',
phone: '+911234567890',
address: { country: 'IN' },
},
paymentMethodDetails: {
paymentMethod: 'FIAT',
paymentType: 'UPI',
upiId: '',
},
deviceInfo: {
userAgent: navigator.userAgent,
screenWidth: window.screen.width,
screenHeight: window.screen.height,
},
confirmSource: 'MERCHANT_SITE',
}),
}
);

const result = await confirmResponse.json();

// 5. Redirect to TSP payment page
if (result.success && result.data.redirectUrl) {
window.location.href = result.data.redirectUrl;
}

For INR payments, the redirectUrl opens a TSP payment page that shows a QR code. On mobile, customers can't scan a QR code on the same device they're paying from. ZenPays provides tools to extract the upi:// deep link from the QR code so you can:

  • Mobile: Open the customer's UPI app directly (GPay, PhonePe, Paytm) with one tap
  • Desktop: Show your own clean QR code + "Copy UPI Link" button

Quick Start: Poll ZenPays API

After confirming payment, poll the ZenPays API to get the extracted UPI link:

// After confirm returns redirectUrl for INR payment:
if (result.data.redirectUrl && currency === 'INR') {
const isMobile = /Mobi|Android|iPhone/i.test(navigator.userAgent);

// Extract cashierOrderNo from redirectUrl for faster lookup
let cashierOrderNo = '';
try {
cashierOrderNo = new URL(result.data.redirectUrl).searchParams.get('orderNo') || '';
} catch {}

// Poll every 4 seconds (ZenPays extracts the QR server-side)
const interval = setInterval(async () => {
const params = new URLSearchParams();
if (cashierOrderNo) params.set('cashierOrderNo', cashierOrderNo);
params.set('payPageUrl', result.data.redirectUrl);

const res = await fetch(
`https://api.zenpayz.com/payment/api/v1/payment-intent/${intentId}/upi-link?${params}`
);
const data = await res.json();

if (data.success && data.data?.upiDeepLink) {
clearInterval(interval);
const upiLink = data.data.upiDeepLink;

if (isMobile && upiLink.startsWith('upi://')) {
// Mobile: open UPI app with brief delay
setTimeout(() => { window.location.href = upiLink; }, 600);
} else {
// Desktop: show your own QR + copy button (don't redirect)
showDesktopUpiUI(upiLink);
}
}
}, 4000);

// Stop polling after 60 seconds
setTimeout(() => clearInterval(interval), 60000);
}

Full Guide

For complete implementation with multiple approaches (server-side headless browser extraction, frontend screen capture), code examples in JavaScript, Python, and PHP, and best practices, see the UPI Native Link Guide.