Node.js
The @wajub/node server SDK, its resources, and the TypeScript it ships with.
@wajub/node is your server's Wajub client: with your secret key, it creates payments, reads their
status and verifies webhook signatures. It never runs in a browser.
@wajub/node
Stable · GAnpm
- Version
- 2.0.0
- Runtime
- Node.js 18+, Bun, Deno, any runtime with fetch
Covers
- Payments
- Billing
- Transfers
- Sync
- Shield
- Tax
Install
npm install @wajub/nodeIt works with both import and require.
Create the client once
Build it in a module and import that module everywhere.
import { Wajub } from '@wajub/node';
if (!process.env.WAJUB_API_KEY) {
throw new Error('WAJUB_API_KEY is missing');
}
export const wajub = new Wajub({
secretKey: process.env.WAJUB_API_KEY,
webhookSecret: process.env.WAJUB_WEBHOOK_SECRET,
});Pass the key yourself
@wajub/node does not read WAJUB_API_KEY from the environment, and the constructor throws
without a key, so read the variable yourself, as above. The webhook secret does fall back to
WAJUB_WEBHOOK_SECRET.
| Option | What it does |
|---|---|
secretKey | Your sk. or sk_test. key |
webhookSecret | The whsec_ secret, needed only for webhooks.constructEvent() |
fetchOptions | A RequestInit merged into every call, for a proxy dispatcher or a dev certificate |
timeout | Request timeout in milliseconds, also settable per call |
The first call
import { wajub } from '@/lib/wajub';
const payment = await wajub.payments.create({
amount: 25000,
currency: 'XAF',
email: 'amina@example.com',
description: 'Order 4172',
reference: 'order-4172',
callback: 'https://shop.example.com/complete',
expires: { in: 60 }, // minutes before the payment expires
});
// payment.authorization_url is where the customer pays.
// payment.authorization_token is what the browser SDK mounts.The response fields keep the API's snake_case, so authorization_url and created_at read the
same here as they do in the API reference. Only method names are
camelCase.
Amounts are in the major unit
25000 with XAF is twenty-five thousand francs. A decimal currency takes a decimal:
amount: 12.5 with GHS.
Every resource on the client
| Getter | Methods |
|---|---|
| wajub.global | ping, channels, countries, currencies |
| wajub.payments | create, retrieve, list, cancel, process, processSplit, listRefunds |
| wajub.customers | create, retrieve, update, delete, list, block, unblock, activate, deactivate, listTaxIds, createTaxId, deleteTaxId |
| wajub.refunds | create, retrieve, list |
| wajub.transfers | create, retrieve, list |
| wajub.beneficiaries | create, retrieve, update, delete, list |
| wajub.links | create, retrieve, update, delete, list |
| wajub.invoices | create, retrieve, update, delete, list, send, markPaid, cancel |
| wajub.accounts | create, retrieve, update, delete, list, regenerateToken |
| wajub.webhookEndpoints | create, retrieve, update, delete, list, rotateSecret |
| wajub.balance | retrieve |
| wajub.events | list, retrieve, resend |
| wajub.disputes | list, retrieve, submitEvidence, accept, close, sendMessage |
| wajub.identity | resolve, validate |
| wajub.tax | getSettings, updateSettings, rates, calculate, reports, listCodes, retrieveCode, listRegistrations, createRegistration, retrieveRegistration, updateRegistration, deleteRegistration, jurisdictions, thresholds, thresholdAlerts |
| wajub.shield | getSettings, updateSettings, stats, listBlocklist, addToBlocklist, removeFromBlocklist |
| wajub.webhooks | constructEvent |
webhookssignature verification runs locally, no HTTP call. All other resources call the merchant REST API.links,invoices,taxandshieldare live only. A sandbox key gets403 This feature is only available in live mode.on every one of their methods.refundsandtransfersare create, retrieve and list only.
Paging through a list
list() returns a page, not an array. It carries the rows, the metadata, and a way to get the
next one.
const page = await wajub.payments.list({ status: 'succeeded', per_page: 50 });
// One page at a time, when you control the loop.
for (const payment of page.data) {
console.log(payment.id, payment.amount);
}
if (page.has_more) {
const next = await page.getNextPage();
}
// Or let it fetch the following pages for you.
for await (const payment of page) {
await reconcile(payment);
}page.meta carries the list's pagination fields, described on Pagination.
Idempotency and retries
Temporary failures are retried for you. To make a create safe to repeat on your side too, after a restart or in a job retry, pass your own idempotency key, the order number for instance.
await wajub.payments.create(params, { idempotencyKey: `order-${orderId}` });Acting for a connected account
Marketplaces pass a connected account per call rather than holding a second client.
await wajub.payments.create(
{ amount: 25000, currency: 'XAF', email: buyer.email },
{ sync: seller.wajubAccountId },
);Setup and capabilities are on Sync.
Webhooks
constructEvent verifies the signature and gives you the parsed event. It needs the body exactly
as it arrived, byte for byte.
import express from 'express';
import { wajub } from './lib/wajub';
const app = express();
app.post(
'/webhooks/wajub',
express.raw({ type: 'application/json' }),
(req, res) => {
try {
const event = wajub.webhooks.constructEvent(
req.body,
req.header('x-wajub-signature')!,
req.header('x-wajub-timestamp')!,
);
if (event.event === 'payment.succeeded') {
void fulfil(event.data);
}
res.sendStatus(200);
} catch {
res.sendStatus(400);
}
},
);express.json() destroys the signature
The signature is computed over the raw body, and a parsed then re-serialised body no longer
matches it. Mount express.raw() on the webhook route only,
before any global JSON parser, or read req.text() in a route handler as the Next.js tab does.
If your clocks drift, constructEvent takes the tolerance in seconds as a fourth argument. Details
on
Signature verification.
Errors
import {
WajubError,
WajubInvalidRequestError,
WajubRateLimitError,
} from '@wajub/node';
try {
await wajub.payments.create(params);
} catch (error) {
if (error instanceof WajubInvalidRequestError) {
return res.status(422).json({ fields: error.errors });
}
if (error instanceof WajubRateLimitError) {
return res.status(503).set('Retry-After', String(error.retryAfter ?? 5)).end();
}
if (error instanceof WajubError) {
logger.error({ code: error.code, status: error.httpStatus, raw: error.raw });
}
throw error;
}| Class | Raised on |
|---|---|
WajubAuthenticationError | 401 and 403 |
WajubInvalidRequestError | 400, 404 and 422 |
WajubPaymentError | 402 |
WajubRateLimitError | 429, with retryAfter in seconds |
WajubApiError | Every other status |
WajubConnectionError | No response at all, network or timeout |
WebhookSignatureVerificationError | A webhook that did not verify |
All of them extend WajubError and carry message, code, httpStatus, errors and raw. The
property is httpStatus, not status.
TypeScript
The types ship inside the package, so there is no @types/wajub to install and nothing to
configure. Everything is exported.
import type {
CreatePaymentParams,
PaymentObject,
PaymentListParams,
CustomerObject,
RefundObject,
TransferObject,
InvoiceObject,
DisputeObject,
EventObject,
WebhookEvent,
PagedResult,
RequestOptions,
WajubConfig,
} from '@wajub/node';Charging without the hosted page
When you collect the payer's details yourself, create the payment first and then process it on a channel.
const payment = await wajub.payments.create({
amount: 25000,
currency: 'XAF',
phone: '+237670000000',
});
const result = await wajub.payments.process(payment.id, {
channel: 'cm.mtn',
phone: '+237670000000',
});A channel is country.operator, so cm.mtn is MTN Mobile Money in Cameroon. The full list is on
Mobile Money. The customer still has to approve on their handset, so the
outcome arrives on the webhook, not in result.
Paying out
const transfer = await wajub.transfers.create(
{
amount: 100000,
currency: 'XAF',
beneficiary: {
name: 'Amina Diallo',
channel: 'cm.mtn',
phone: '+237670000000',
},
reference: 'payout-892',
},
{ idempotencyKey: 'payout-892' },
);beneficiary also accepts the ben_… id of a saved beneficiary, which is the better shape once
you pay the same person twice.
Local development
The SDK always talks to the real API, so a sandbox key is what makes a call harmless. To receive webhooks on your machine, forward them with the CLI rather than exposing a tunnel.
wajub listen --forward-to localhost:3000/webhooks/wajub
wajub trigger payment.succeededMore on the CLI.
Related pages
- Wajub.jsThe browser half, mounted with the token this SDK returns.
- ReactProvider and components for a Next.js front end.
- WebhooksEvery event, and the delivery guarantees behind them.
- IdempotencyWhat a key protects and for how long.
- Error handlingWhich failures to retry and which to surface.
- API referenceThe endpoints behind every method above.