# Introduction (/docs) `@payments-sdk/payments` gives applications one configured interface for provider checkout and verification while leaving HTTP routes, persistence, and settlement in the application. ## Install for Agents [#install-for-agents] ```bash npx skills add neplextech/payments-sdk ``` Agents can load the project index at [https://payments.neplex.dev/llms.txt](https://payments.neplex.dev/llms.txt) or the full reference at [https://payments.neplex.dev/llms-full.txt](https://payments.neplex.dev/llms-full.txt). ## The boundary [#the-boundary] The runtime does not know which providers exist. Install the provider packages you need and pass their instances to `createPayments()`: ```ts import { fonepay } from '@payments-sdk/fonepay'; import { createPayments } from '@payments-sdk/payments'; const payments = createPayments({ providers: [ fonepay({ merchantCode, secretKey, username, password, }), ], }); ``` The configured provider IDs and capabilities drive the resulting TypeScript surface. A provider that does not implement returns or webhooks does not add those namespaces to the inferred runtime. ## Start here [#start-here] ## What the runtime owns [#what-the-runtime-owns] `@payments-sdk/payments` validates normalized payment inputs, selects a configured provider, supplies the runtime `fetch` and clock, and returns normalized actions and statuses. It does not create HTTP routes, store payment records, or decide when an order is fulfilled. The application owns those boundaries. A typical flow is: 1. Create an application payment record and a payment intent. 2. Call `payments.checkout()` and execute the returned action in the browser. 3. Accept the provider's return or webhook in an application route. 4. Call `payments.verify()` where the provider supports it. 5. Persist settlement idempotently and fulfill the order. ## Important distinction [#important-distinction] A browser return is a request to parse, not proof that an order is settled. Use a provider's verification capability before fulfilling an order, and make the final state change idempotent in your application's persistence boundary. # Installation (/docs/installation) `@payments-sdk/payments` is the orchestration runtime. Provider integrations are separate packages, so an application installs only the providers it intends to configure. ## Install for Agents [#install-for-agents] ```bash npx skills add neplextech/payments-sdk ``` Use [https://payments.neplex.dev/llms.txt](https://payments.neplex.dev/llms.txt) for the concise agent index and [https://payments.neplex.dev/llms-full.txt](https://payments.neplex.dev/llms-full.txt) for the complete reference. ## Install a provider [#install-a-provider] For example, install the runtime with FonePay: ```bash npm install @payments-sdk/payments @payments-sdk/fonepay ``` The equivalent pnpm command is: ```bash pnpm add @payments-sdk/payments @payments-sdk/fonepay ``` Install more provider packages when the application needs them: ```bash npm install @payments-sdk/payments @payments-sdk/esewa @payments-sdk/khalti ``` Providers are not bundled into `@payments-sdk/payments`. This keeps the runtime provider-independent and lets provider packages be published and upgraded independently. ## Configure on the server [#configure-on-the-server] Create the configured runtime in server-only application code. Keep gateway credentials out of browser bundles and source control: ```ts import { esewa } from '@payments-sdk/esewa'; import { fonepay } from '@payments-sdk/fonepay'; import { createPayments } from '@payments-sdk/payments'; export const payments = createPayments({ providers: [ fonepay({ merchantCode: process.env.FONEPAY_MERCHANT_CODE!, secretKey: process.env.FONEPAY_SECRET_KEY!, username: process.env.FONEPAY_USERNAME!, password: process.env.FONEPAY_PASSWORD!, }), esewa({ productCode: process.env.ESEWA_PRODUCT_CODE!, secretKey: process.env.ESEWA_SECRET_KEY!, environment: 'sandbox', }), ], }); ``` The provider IDs are literal types. With the configuration above, `payments.checkout({ provider: 'fonepay', ... })` and `payments.checkout({ provider: 'esewa', ... })` receive different, provider-specific `providerOptions` types. ## Runtime dependencies [#runtime-dependencies] The runtime uses Web platform primitives and does not start a server or connect to a database. Applications can inject `fetch` and `now` for tests, proxies, and observability: ```ts const payments = createPayments({ providers: [/* configured provider instances */], fetch: tracedFetch, now: () => clock.now(), }); ``` # Quick start (/docs/quick-start) 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 [#1-configure-the-runtime] ```ts // 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 [#2-create-checkout] Use integer minor units for amounts. For NPR, `1500_00` means 1,500 rupees: ```ts 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: ```ts 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 [#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: ```ts 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 [#4-handle-a-return-then-verify] The application owns the route. Pass the raw request to the typed returns namespace: ```ts 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. # NestJS integration (/docs/guides/nestjs) NestJS owns the HTTP and application boundaries. `@payments-sdk/payments` remains a plain configured object that performs provider operations. Exporting that object from a small module keeps the provider configuration outside Nest's dependency-injection lifecycle while services and controllers can import it. ## Configure once [#configure-once] ```ts // payments.ts import { esewa } from '@payments-sdk/esewa'; import { fonepay } from '@payments-sdk/fonepay'; import { createPayments } from '@payments-sdk/payments'; export const payments = createPayments({ providers: [ fonepay({ merchantCode, secretKey, username, password }), esewa({ productCode, secretKey: esewaSecretKey, environment: 'sandbox', }), ], }); ``` The values above should come from Nest's configuration system or a secret manager in a real application. Keep this module server-only. ## Application service [#application-service] The service owns persistence, idempotency, and settlement. The SDK does not provide `createPayment()` or `settlePayment()`; those are application business operations around the SDK: ```ts // payment.service.ts import { Injectable } from '@nestjs/common'; import { payments } from './payments.js'; @Injectable() export class PaymentService { async createPayment(orderId: string) { const order = await this.orders.findForCheckout(orderId); const result = await payments.checkout({ provider: order.provider, amount: order.amountMinor, currency: order.currency, reference: order.paymentReference, providerOptions: order.providerOptions, }); await this.payments.saveAction(order.paymentReference, result); return result; } async settlePayment(reference: string) { const payment = await this.payments.findByReference(reference); if (!payment || payment.settledAt) return payment; const verified = await payments.verify({ provider: payment.provider, reference, providerOptions: payment.verifyOptions, }); if (verified.status !== 'succeeded') return verified; // Use a transaction or an equivalent idempotency boundary. return this.payments.markSettledAndFulfill(payment.id, verified); } } ``` `markSettledAndFulfill()` must be safe when called more than once. A provider return, a retry, and a reconciliation job may all attempt settlement for the same payment. ## Controller routes [#controller-routes] Controllers translate HTTP requests into calls to the service. The SDK does not register these routes: ```ts // payment.controller.ts import { Controller, Get, Param, Post, Req } from '@nestjs/common'; import type { Request } from 'express'; @Controller('payments') export class PaymentController { constructor(private readonly service: PaymentService) {} @Post(':orderId/checkout') createCheckout(@Param('orderId') orderId: string) { return this.service.createPayment(orderId); } @Get('returns/esewa') async esewaReturn(@Req() request: Request) { const returned = await payments.returns.esewa(request); return this.service.settlePayment(returned.reference!); } } ``` When using Express, pass the request shape that preserves the raw URL and body needed by the provider. A Web `Request` or the normalized raw request shape is also supported by the SDK. ## Webhook route [#webhook-route] Webhook support is capability-driven. Only add a typed `payments.webhooks.` call for a configured provider that implements a webhook parser: ```ts @Post('webhooks/provider-name') async providerWebhook(@Req() request: Request) { const event = await payments.webhooks.providerName(request); return this.service.settlePayment(event.reference!); } ``` The initial eSewa, FonePay, and Khalti packages expose returns where applicable, but do not expose webhooks. A future webhook-capable provider adds that namespace through its own package without changing `@payments-sdk/payments`. Do not treat a browser return and a webhook as the same event. Returns are browser-facing inbound requests; webhooks are server-to-server events. Both should be verified and settled idempotently according to the provider contract. # Provider authoring (/docs/guides/provider-authoring) A third party can publish `@somebody/payments-provider` without changing `@payments-sdk/payments`. The provider implements the structural contracts from `@payments-sdk/core`, and the runtime derives its typed methods and capability namespaces from the configured provider object. ## Package boundary [#package-boundary] Use the core package as a development-time contract: ```bash npm install @payments-sdk/payments npm install --save-dev @payments-sdk/core typescript ``` Provider source should use type-only imports from core. The published provider JavaScript must not contain a runtime import of `@payments-sdk/core`. ## A small provider [#a-small-provider] This fake provider creates a redirect and supports verification. The literal `'acme-pay'` ID is what gives consumers a narrowed provider key: ```ts import type { CheckoutHandler, PaymentProvider, VerifyHandler, } from '@payments-sdk/core'; interface AcmeCheckoutOptions { readonly returnUrl: string; } interface AcmeVerifyOptions { readonly merchantReference?: string; } interface AcmeCheckoutData { readonly paymentId: string; } interface AcmeVerifyData { readonly gatewayStatus: string; } const checkout: CheckoutHandler< 'acme-pay', AcmeCheckoutOptions, AcmeCheckoutData > = async ({ input, fetch }) => { const response = await fetch('https://gateway.example/payments', { method: 'POST', body: JSON.stringify({ amount: input.intent.amount, reference: input.intent.reference, returnUrl: input.providerOptions.returnUrl, }), }); if (!response.ok) throw new Error('Acme Pay request failed'); const body = (await response.json()) as { paymentId: string; paymentUrl: string; }; return { provider: 'acme-pay', status: 'pending', action: { type: 'redirect', url: body.paymentUrl }, providerData: { paymentId: body.paymentId }, }; }; const verify: VerifyHandler< 'acme-pay', AcmeVerifyOptions, AcmeVerifyData > = async ({ input, fetch }) => { const response = await fetch( `https://gateway.example/payments/${input.reference}`, ); if (!response.ok) throw new Error('Acme Pay lookup failed'); const body = (await response.json()) as { status: string }; const status = body.status === 'paid' ? 'succeeded' : 'unknown'; return { provider: 'acme-pay', status, providerData: { gatewayStatus: body.status }, }; }; export function acmePay(): PaymentProvider<'acme-pay'> { return { id: 'acme-pay', checkout, verify, } satisfies PaymentProvider<'acme-pay'>; } ``` The example intentionally maps only an explicitly recognized gateway status to `succeeded`. Unknown or ambiguous states must remain non-successful. ## Capabilities are optional [#capabilities-are-optional] Add only capabilities the gateway really supports: ```ts return { id: 'acme-pay', checkout, verify, returns: parseReturn, // Add webhooks only when the gateway signs and sends server events. } satisfies PaymentProvider<'acme-pay'>; ``` The consumer's type surface follows those properties. A provider without `returns` does not add `payments.returns.acmePay`, and a provider without `verify` cannot be passed to `payments.verify()`. ## Inbound parsing [#inbound-parsing] Returns and webhooks receive an `InboundRequest` and runtime context. Keep the raw URL and body available because gateways may send malformed query strings or signatures over the original wire representation: ```ts import type { ReturnsHandler } from '@payments-sdk/core'; const parseReturn: ReturnsHandler< 'acme-pay', { gatewayStatus: string } > = async (request) => { const url = request instanceof Request ? request.url : request.url; const query = new URL(url).searchParams; return { provider: 'acme-pay', status: 'pending', reference: query.get('reference') ?? undefined, providerData: { gatewayStatus: query.get('status') ?? 'missing' }, }; }; ``` Validate signatures before exposing a webhook event as trusted input. A browser return is normally an unauthoritative claim and should be followed by provider verification. ## Testing and publishing [#testing-and-publishing] Provider tests should cover: * request URLs, methods, headers, and signed canonical fields; * successful, failed, pending, and unknown gateway statuses; * malformed callbacks and missing required fields; * no runtime import of `@payments-sdk/core` in the built JavaScript; * deterministic behavior through injected `fetch` and `now`. Keep credentials out of errors, serialized provider data, and test fixtures. Build the provider as ESM and publish it independently. Consumers then add it to the runtime with no change to the `@payments-sdk/payments` package: ```ts import { acmePay } from '@somebody/payments-provider'; import { createPayments } from '@payments-sdk/payments'; const payments = createPayments({ providers: [acmePay()] }); ``` # eSewa (/docs/providers/esewa) `@payments-sdk/esewa` implements eSewa ePay v2 signed form checkout, transaction verification, and signed browser returns. ## Install and configure [#install-and-configure] ```bash npm install @payments-sdk/payments @payments-sdk/esewa ``` ```ts import { esewa } from '@payments-sdk/esewa'; const provider = esewa({ productCode: process.env.ESEWA_PRODUCT_CODE!, secretKey: process.env.ESEWA_SECRET_KEY!, environment: 'sandbox', }); ``` Use `environment: 'sandbox'` with sandbox credentials and switch to `'production'` only with production credentials and registered return URLs. The application supplies credentials; the package does not read environment variables automatically. ## Form checkout [#form-checkout] ```ts 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, }, }); if (result.action.type === 'form') { // Render a POST form using result.action.url and result.action.fields. } ``` Amounts and optional charges are integer minor units. eSewa signs the formatted `total_amount`, `transaction_uuid`, and `product_code` fields. Persist the exact charged amount when the later verification call will need it. ## Browser return [#browser-return] Pass the raw request from the application route: ```ts export async function esewaReturn(request: Request) { const returned = await payments.returns.esewa(request); // Parse success is not settlement. Verify before fulfilling the order. return payments.verify({ provider: 'esewa', reference: returned.reference!, providerOptions: { totalAmount: 1500_00 }, }); } ``` The return handler decodes and validates eSewa's signed base64 `data` payload. It also accepts callback URLs where parameters are separated by a second `?` and preserves literal `+` characters in base64 data for compatibility with malformed gateway callbacks. ## Verification [#verification] Verification requires the exact total charged: ```ts const result = await payments.verify({ provider: 'esewa', reference: 'order_123', providerOptions: { totalAmount: 1500_00 }, }); ``` Only eSewa's `COMPLETE` status becomes `succeeded`. Ambiguous, not-found, and unknown statuses remain `unknown`. ## Capabilities and security [#capabilities-and-security] eSewa implements `checkout`, `verify`, and `returns`. It does not implement webhooks, refunds, or cancellations. Keep `secretKey` server-side and treat the return as input to verification, even though its signature is validated. # FonePay (/docs/providers/fonepay) `@payments-sdk/fonepay` implements FonePay Dynamic QR checkout and transaction status verification. ## Install and configure [#install-and-configure] ```bash npm install @payments-sdk/payments @payments-sdk/fonepay ``` ```ts import { fonepay } from '@payments-sdk/fonepay'; const provider = fonepay({ merchantCode: process.env.FONEPAY_MERCHANT_CODE!, secretKey: process.env.FONEPAY_SECRET_KEY!, username: process.env.FONEPAY_USERNAME!, password: process.env.FONEPAY_PASSWORD!, }); ``` The package uses FonePay's development client API hosts by default. If a merchant contract supplies different hosts or paths, configure both endpoints explicitly: ```ts const provider = fonepay({ merchantCode, secretKey, username, password, endpoints: { generateQr: 'https://merchant.example/qr', status: 'https://merchant.example/status', }, }); ``` Confirm endpoint hosts and signing fields against the merchant contract before using production credentials. ## Dynamic QR checkout [#dynamic-qr-checkout] ```ts const result = await payments.checkout({ provider: 'fonepay', amount: 1500_00, currency: 'NPR', reference: 'order_123', providerOptions: { remarks1: 'Order 123', remarks2: 'Online checkout', }, }); if ( result.action.type === 'qr' && result.action.format === 'payload' ) { // Give result.action.data to the application's QR renderer. return result.action.data; } ``` Amounts are integer paisa. The provider sends FonePay a two-decimal rupee amount. The normalized action contains the QR payload; the SDK does not render QR images or manage a QR WebSocket connection. The provider result data can include the generated PRN, QR message, and an optional third-party QR WebSocket URL. Persist the reference/PRN in the application if later reconciliation needs it. ## Verification [#verification] The application reference is sent as FonePay's PRN: ```ts const verified = await payments.verify({ provider: 'fonepay', reference: 'order_123', providerOptions: {}, }); ``` Documented FonePay statuses are normalized to the shared status model. A missing or unrecognized status is `unknown`, never `succeeded`. ## Capabilities and security [#capabilities-and-security] FonePay implements `checkout` and `verify`. It does not implement browser returns, webhooks, refunds, or cancellations. Keep the secret key, username, and password on the server, and do not log signed request bodies. # Provider architecture (/docs/providers) Providers are independently published packages. The application chooses which ones to install and configures them explicitly: ```ts import { esewa } from '@payments-sdk/esewa'; import { fonepay } from '@payments-sdk/fonepay'; import { createPayments } from '@payments-sdk/payments'; const payments = createPayments({ providers: [ esewa({ productCode, secretKey, environment: 'sandbox' }), fonepay({ merchantCode, secretKey, username, password }), ], }); ``` `@payments-sdk/payments` does not import, register, or maintain a list of providers. A future provider package can implement the core protocol without changing the runtime package. ## Capabilities [#capabilities] Provider capabilities are optional and meaningful: | Capability | Purpose | | ---------- | --------------------------------------------------- | | `checkout` | Creates a redirect, form, or QR payment action. | | `verify` | Queries the provider's authoritative payment state. | | `returns` | Parses a browser return request. | | `webhooks` | Parses a server-to-server event. | | `refund` | Requests a refund when the provider supports it. | | `cancel` | Cancels a payment when the provider supports it. | The initial provider packages expose these capabilities: | Provider | Checkout | Verify | Returns | Webhooks | | -------- | ---------- | ------ | -------------------- | -------- | | eSewa | Form POST | Yes | Signed return | No | | FonePay | QR payload | Yes | No | No | | Khalti | Redirect | Yes | Informational return | No | ## Provider-specific types [#provider-specific-types] Provider options and result data remain specific to each provider, while the runtime normalizes the common status and action shapes. This keeps the shared surface predictable without hiding gateway-specific details in `providerData`. For provider authoring contracts, install `@payments-sdk/core` as a development dependency and use type-only imports. Provider code receives the runtime `fetch` and `now` context from the application runtime, which keeps it testable and avoids taking ownership of global infrastructure. # Khalti (/docs/providers/khalti) `@payments-sdk/khalti` implements Khalti KPG-2 Web Checkout redirect checkout, payment lookup, and browser return parsing. ## Install and configure [#install-and-configure] ```bash npm install @payments-sdk/payments @payments-sdk/khalti ``` ```ts import { khalti } from '@payments-sdk/khalti'; const provider = khalti({ secretKey: process.env.KHALTI_SECRET_KEY!, environment: 'sandbox', websiteUrl: 'https://merchant.example', }); ``` Use the sandbox environment and credentials for development. The application supplies credentials and the package does not load them from the environment. ## Redirect checkout [#redirect-checkout] ```ts const result = await payments.checkout({ provider: 'khalti', amount: 1500_00, currency: 'NPR', reference: 'order_123', providerOptions: { returnUrl: 'https://merchant.example/returns/khalti', purchaseOrderName: 'Order 123', customerInfo: { name: 'Ada Example', email: 'ada@example.com', phone: '9800000000', }, }, }); if (result.action.type === 'redirect') { return Response.redirect(result.action.url); } ``` The amount is sent as integer paisa. The result includes a redirect action and provider data containing the Khalti `pidx`, payment URL, and optional expiry. Persist the `pidx` alongside the application payment; the provider package is stateless. ## Return parsing and lookup [#return-parsing-and-lookup] Khalti browser returns are not signed. The return handler extracts `pidx` and known fields but always reports `pending`, regardless of a claimed callback status: ```ts export async function khaltiReturn(request: Request) { const returned = await payments.returns.khalti(request); const verified = await payments.verify({ provider: 'khalti', reference: returned.reference!, providerOptions: {}, }); return { returned, verified }; } ``` For Khalti, the verification reference is the returned `pidx`. Only the gateway's `Completed` lookup status becomes `succeeded`. Pending, expired, cancelled, refunded, partially refunded, and unknown values remain distinct normalized states. ## Capabilities and security [#capabilities-and-security] Khalti implements `checkout`, `verify`, and `returns`. It does not implement webhooks, refunds, or cancellations. Keep `secretKey` server-side; it is sent as an authorization header. Never fulfill an order from the browser return alone.