Node.js SDK
@n1co/epay calls the ePay API from your Node.js server, with TypeScript types, coded errors and webhook verification.
- Install
@n1co/epayon your server (Node.js 22.12+, 24 or 26+). - Create the client with your secret key and the development host.
- Create intents with
paymentIntents.create()and verify webhooks withconstructEvent().
npm install @n1co/epayA complete Express server: it creates the intent and receives the webhook.
server.mjs
import express from 'express';
import { N1co, WebhookSignatureError } from '@n1co/epay';
const n1co = new N1co(process.env.N1CO_SECRET_KEY, {
baseUrl: 'https://api-epay.dev.n1co.dev', // development
});
const app = express();
// The webhook goes before express.json(): it needs the raw body.
app.post('/webhooks/n1co', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = N1co.webhooks.constructEvent(req.body, req.headers, process.env.N1CO_WEBHOOK_SECRET);
} catch (err) {
if (err instanceof WebhookSignatureError) return res.status(400).send('invalid signature');
throw err;
}
if (event.type === 'payment_intent.succeeded') {
// Fulfill the order in event.data.object.reference; skip repeated event.id values.
}
res.sendStatus(204);
});
app.use(express.json());
app.post('/checkout/payment-intent', async (req, res) => {
// The amount comes from your order, never from the browser.
const intent = await n1co.paymentIntents.create({
amount: 1250,
currency: 'USD',
customer: { id: 'cust-1001', name: 'Ana López', email: 'ana.lopez@example.com', phone: '+503 7000-1234' },
reference: 'order-8812',
});
res.json({ client_secret: intent.client_secret });
});
app.listen(3000);The package ships ES modules and CommonJS, with no dependencies: const { N1co } = require('@n1co/epay');. The page confirms the payment as in Charge.
Webhooks with NestJS#
Turn on rawBody when you create the app and verify req.rawBody:
webhooks.controller.ts
// main.ts: const app = await NestFactory.create(AppModule, { rawBody: true });
import { BadRequestException, Controller, Headers, HttpCode, Post, Req } from '@nestjs/common';
import type { RawBodyRequest } from '@nestjs/common';
import type { Request } from 'express';
import { N1co, WebhookSignatureError } from '@n1co/epay';
@Controller('webhooks')
export class WebhooksController {
@Post('n1co')
@HttpCode(204)
handle(@Req() req: RawBodyRequest<Request>, @Headers() headers: Record<string, string>) {
try {
const event = N1co.webhooks.constructEvent(req.rawBody!, headers, process.env.N1CO_WEBHOOK_SECRET!);
// Handle event.type…
} catch (err) {
if (err instanceof WebhookSignatureError) throw new BadRequestException();
throw err;
}
}
}Errors#
Every SDK error has code (the API's stable code), status, details and requestId. Branch on code, never on the message.
import { isN1coError } from '@n1co/epay';
try {
await n1co.paymentIntents.create(params);
} catch (err) {
if (!isN1coError(err)) throw err;
if (err.status === 400) console.log(err.details?.field, err.details?.code);
else console.log(err.code, err.status, err.requestId);
}The codes are in Errors. Include the requestId when you contact n1co.
Common mistakes#
- Parsing the body before verifying. With
express.json()on the webhook route, the signature does not match. Useexpress.raw()orrawBody: true. - Retrying a POST blindly. The SDK never retries a POST: the API has no idempotency keys. After a
ConnectionError, read the intent or wait for its webhook before creating another one. instanceofwith both ESM and CommonJS. If your app loads both copies, useisN1coError(err)withcodeandstatus.- Leaving out
baseUrl. Without it, the client uses the production host, which arrives with the production release.
The client options, methods, types and GET retries are in the package README.