Payments SDK

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.

On this page