# 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.