Quick start
Configure a provider, create a checkout action, and verify payment state.
This example uses eSewa because its checkout action is a form POST and its browser return and verification steps show the separation between the browser and settlement boundaries.
1. Configure the runtime
// payments.ts
import { esewa } from '@payments-sdk/esewa';
import { createPayments } from '@payments-sdk/payments';
export const payments = createPayments({
providers: [
esewa({
productCode: process.env.ESEWA_PRODUCT_CODE!,
secretKey: process.env.ESEWA_SECRET_KEY!,
environment: 'sandbox',
}),
],
});The import from @payments-sdk/esewa is intentional. The runtime does
not contain a provider registry or re-export provider factories.
2. Create checkout
Use integer minor units for amounts. For NPR, 1500_00 means 1,500
rupees:
const result = await payments.checkout({
provider: 'esewa',
amount: 1500_00,
currency: 'NPR',
reference: 'order_123',
providerOptions: {
successUrl: 'https://merchant.example/returns/esewa',
failureUrl: 'https://merchant.example/payments/failed',
taxAmount: 0,
productServiceCharge: 0,
productDeliveryCharge: 0,
},
});Checkout does not render the result. Pass the action to application code that knows how to render the browser experience:
switch (result.action.type) {
case 'redirect':
return Response.redirect(result.action.url);
case 'form':
return Response.json({
method: result.action.method,
url: result.action.url,
fields: result.action.fields,
});
case 'qr':
return Response.json({ payload: result.action.data });
}FonePay returns a QR payload, eSewa returns a POST form, and Khalti
returns a redirect. They all use the same checkout call while keeping
their gateway details in provider-specific providerData.
3. Prefer an intent for multi-step flows
When the application needs to store or inspect the payment description before choosing a provider, create an immutable intent first:
const intent = payments.createIntent({
amount: 1500_00,
currency: 'NPR',
reference: 'order_123',
metadata: { orderId: 'order_123' },
});
const result = await payments.checkout({
intent,
provider: 'esewa',
providerOptions: {
successUrl: 'https://merchant.example/returns/esewa',
failureUrl: 'https://merchant.example/payments/failed',
taxAmount: 0,
productServiceCharge: 0,
productDeliveryCharge: 0,
},
});An intent is not persisted by the SDK. Store the application payment and intent data in the application's database when the flow requires durability.
4. Handle a return, then verify
The application owns the route. Pass the raw request to the typed returns namespace:
export async function GET(request: Request) {
const returned = await payments.returns.esewa(request);
const verified = await payments.verify({
provider: 'esewa',
reference: returned.reference!,
providerOptions: { totalAmount: 1500_00 },
});
if (verified.status !== 'succeeded') {
return new Response('Payment is not settled', { status: 409 });
}
// Application-owned, idempotent settlement and order fulfillment.
await settleOrderOnce(returned.reference!);
return Response.redirect(
'https://merchant.example/orders/order_123',
);
}The return is an input, not a replacement for provider verification. A successful browser redirect must not by itself mark an order paid.