Skip to content

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.

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/node

It works with both import and require.

Create the client once

Build it in a module and import that module everywhere.

lib/wajub.ts
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,
});
OptionWhat it does
secretKeyYour sk. or sk_test. key
webhookSecretThe whsec_ secret, needed only for webhooks.constructEvent()
fetchOptionsA RequestInit merged into every call, for a proxy dispatcher or a dev certificate
timeoutRequest timeout in milliseconds, also settable per call

The first call

Create a payment
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

GetterMethods
wajub.globalping, channels, countries, currencies
wajub.paymentscreate, retrieve, list, cancel, process, processSplit, listRefunds
wajub.customerscreate, retrieve, update, delete, list, block, unblock, activate, deactivate, listTaxIds, createTaxId, deleteTaxId
wajub.refundscreate, retrieve, list
wajub.transferscreate, retrieve, list
wajub.beneficiariescreate, retrieve, update, delete, list
wajub.linkscreate, retrieve, update, delete, list
wajub.invoicescreate, retrieve, update, delete, list, send, markPaid, cancel
wajub.accountscreate, retrieve, update, delete, list, regenerateToken
wajub.webhookEndpointscreate, retrieve, update, delete, list, rotateSecret
wajub.balanceretrieve
wajub.eventslist, retrieve, resend
wajub.disputeslist, retrieve, submitEvidence, accept, close, sendMessage
wajub.identityresolve, validate
wajub.taxgetSettings, updateSettings, rates, calculate, reports, listCodes, retrieveCode, listRegistrations, createRegistration, retrieveRegistration, updateRegistration, deleteRegistration, jurisdictions, thresholds, thresholdAlerts
wajub.shieldgetSettings, updateSettings, stats, listBlocklist, addToBlocklist, removeFromBlocklist
wajub.webhooksconstructEvent
  • webhooks signature verification runs locally, no HTTP call. All other resources call the merchant REST API.
  • links, invoices, tax and shield are live only. A sandbox key gets 403 This feature is only available in live mode. on every one of their methods.
  • refunds and transfers are 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.

Two ways to walk a list
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.

Your own idempotency key
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.

One call, one seller
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);
    }
  },
);

If your clocks drift, constructEvent takes the tolerance in seconds as a fourth argument. Details on Signature verification.

Errors

Narrow before you read
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;
}
ClassRaised on
WajubAuthenticationError401 and 403
WajubInvalidRequestError400, 404 and 422
WajubPaymentError402
WajubRateLimitError429, with retryAfter in seconds
WajubApiErrorEvery other status
WajubConnectionErrorNo response at all, network or timeout
WebhookSignatureVerificationErrorA 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.

What you will actually import
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.

Mobile Money push
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

A transfer to a phone number
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.

Two terminals
wajub listen --forward-to localhost:3000/webhooks/wajub
wajub trigger payment.succeeded

More on the CLI.

What did you think of this content?