Skip to content

Developer docs

API reference

Conventions

Base URL
https://api.northpoletradelink.com

Amounts

Amounts are decimal numbers in the currency's major unit — 5000 means ₦5,000.00 and the minimum accepted value is 0.01. They are not minor units, so do not multiply by 100 before sending.

Response envelope

Payout and key-management endpoints wrap their result in a success / data envelope and echo a requestId. Resource endpoints such as business details, wallets and payment sessions return the object directly. Quote both shapes in your client rather than assuming one.

Enveloped response
{  "success": true,  "data": { },  "requestId": "req_1234567890"}

Send an x-request-id header and it is echoed back as requestId, which makes correlating your logs with ours straightforward when you need support.

Business

GET/api/business/detailsx-api-key

Returns public-facing details for the business the key belongs to. The business is resolved from the key, so there is no id to pass.

Response · 200
{  "id": "business_abc123",  "name": "Acme Corp",  "logo": "https://cdn.example.com/logo.png",  "cover_photo": "https://cdn.example.com/cover.png",  "kyb_status": "verified",  "is_active": true}

Response fields

FieldTypeDescription
idstringBusiness identifier.
namestringRegistered business name.
logostring | nullLogo URL, if uploaded.
cover_photostring | nullCover image URL, if uploaded.
kyb_statusstringVerification state. Only a verified business can transact.
is_activebooleanWhether the business is currently enabled.

Wallets

GET/api/business/walletsx-api-key

Lists every wallet the business holds, with its virtual account where one has been issued. Each currency gets its own wallet.

Response · 200
[  {    "wallet_id": "wallet_123",    "currency": "NGN",    "balance": 150000,    "virtual_account": {      "account_number": "1234567890",      "bank_name": "Wema Bank",      "account_name": "Acme Corp"    }  }]

How funding works

  1. 01A customer transfers to your virtual account number.
  2. 02The banking partner detects the incoming transfer and notifies NorthPole.
  3. 03Your wallet is credited automatically.
  4. 04A wallet.funded webhook is delivered to your endpoint.

Payment sessions

A payment session is a time-bound checkout for one customer and amount. NorthPole assigns a dynamic virtual account, and credits your wallet when the matching transfer arrives.

NGN only, one hour

Payment sessions currently support NGN. A session expires 1 hour after creation; the virtual account is then deactivated and a late transfer has to be reconciled manually.

Create a session

POST/api/business/payments/sessionx-api-key
Request body
{  "amount": 5000,  "currency": "NGN",  "customer_name": "John Doe",  "customer_email": "john@example.com",  "customer_phone": "+2348012345678",  "metadata": { "order_id": "ORD-12345" }}

Request fields

FieldTypeDescription
amountrequirednumberDecimal major units. Minimum 0.01.
currencyrequired"NGN"NGN only for payment sessions.
customer_namerequiredstringCustomer's full name.
customer_emailrequiredstringMust be a valid email address.
customer_phonestringRequired for live sessions, which create a dynamic virtual account.
payment_referencestringOptional custom reference used when creating the dynamic virtual account.
metadataobjectArbitrary data echoed back on retrieval.
Response · 201
{  "session_id": "ps_abc123def456",  "checkout_url": "https://checkout.northpoletradelink.com/checkout/ps_abc123def456",  "checkurl": "https://checkout.northpoletradelink.com/checkout/ps_abc123def456",  "amount": 5000,  "currency": "NGN",  "environment": "live",  "expires_at": "2026-08-25T12:00:00Z",  "business": { "name": "Acme Corp" },  "payment_details": {    "account_number": "1234567890",    "bank_name": "Wema Bank",    "account_name": "Acme Corp"  }}

Use checkout_url

checkurl is a compatibility alias for the same value. Treat checkout_url as canonical.

Get session details

GET/api/business/payments/session/:sessionIdPublic

Deliberately unauthenticated so a customer can check their own payment status. Safe to call from your checkout page — it needs no key, so your key never reaches the browser.

Response · 200
{  "session_id": "ps_abc123def456",  "amount": 5000,  "base_fee": 75,  "platform_fee": 25,  "total_fee": 100,  "total_amount": 5100,  "currency": "NGN",  "customer_name": "John Doe",  "customer_email": "john@example.com",  "status": "pending",  "expires_at": "2026-08-25T12:00:00Z",  "created_at": "2026-08-25T11:00:00Z",  "completed_at": null,  "account_details": {    "account_number": "1234567890",    "account_name": "Business Account",    "bank_name": "Wema Bank"  },  "metadata": {}}

Verify a session

GET/api/business/payments/session/:sessionId/verifyx-api-key

Confirms the session belongs to your business and reports whether it was paid. This is the call to gate fulfilment on.

Response · 200
{  "verified": true,  "session_id": "ps_abc123def456",  "status": "completed",  "amount": 5000,  "paid": true,  "completed_at": "2026-08-25T11:45:00Z"}

Settle a test session

POST/api/business/payments/session/:sessionId/settle-testPublic · test only

Completes a test-mode session and credits the test wallet without calling any external provider. It is a checkout simulation, so it requires no API key and has no effect on live sessions.

Response · 200
{  "session_id": "ps_abc123def456",  "status": "completed",  "environment": "test",  "transaction_reference": "test_settle_ps_abc123def456_1712345678"}

Session statuses

Status values

FieldTypeDescription
pendingstringCreated, awaiting payment.
completedstringPayment received and matched.
expiredstringThe hour elapsed without payment.

Customers

POST/api/business/customersx-api-key

Creates a customer record under your business. Identity fields are required because the customer is verified on creation.

Request body
{  "email": "john@example.com",  "password": "SecurePass123",  "first_name": "John",  "last_name": "Doe",  "phone": "+2348012345678",  "dob": "1990-04-12",  "country": "NG",  "address": {    "street": "14 Adeola Odeku Street",    "city": "Lagos",    "state": "Lagos",    "country": "NG",    "postal_code": "106104"  },  "identification_type": "BVN",  "identification_number": "12345678901",  "photo": "https://cdn.example.com/photo.jpg",  "identity": {    "type": "BVN",    "number": "12345678901",    "country": "NG",    "image": "https://cdn.example.com/id.jpg"  }}

Notable fields

FieldTypeDescription
passwordrequiredstringMinimum 6 characters.
dobrequiredstringFormat YYYY-MM-DD.
identification_typerequiredenumBVN, NIN, PASSPORT, DRIVERS_LICENSE or VOTERS_CARD.
addressrequiredobjectstreet, city, state, country and postal_code are all required.
identityrequiredobjecttype, number, country and image are all required.

Payouts

Live mode only

Payout transfers require a live key. A test key returns 400 with an explanatory message. USD wallets cannot be withdrawn directly — use an FX conversion first.

List payout banks

GET/api/business/payout/banksx-api-key
Response · 200
{  "success": true,  "data": [    { "name": "Guaranty Trust Bank", "code": "058" },    { "name": "Access Bank", "code": "044" }  ],  "requestId": "req_1234567890"}

Resolve an account name

POST/api/business/payout/account-lookupx-api-key
Request body
{  "bankCode": "058",  "accountNumber": "0123456789"}
Response · 200
{  "success": true,  "data": {    "bankCode": "058",    "bankName": "Guaranty Trust Bank",    "accountNumber": "0123456789",    "accountName": "JOHN DOE"  },  "requestId": "req_1234567890"}

Confirm before you send

Show the resolved accountName to whoever authorises the payout. It is the last cheap moment to catch a wrong account number.

Initiate a payout

POST/api/business/payout/transferx-api-key · live
Request body
{  "walletId": "507f1f77bcf86cd799439011",  "bankCode": "058",  "accountNumber": "0123456789",  "amount": 10000,  "narration": "Supplier payment"}

Request fields

FieldTypeDescription
amountrequirednumberMust be positive. Decimal major units.
bankCoderequiredstringFrom the bank list.
accountNumberrequiredstringRecipient account number.
walletIdstringSource wallet. Defaults to the business's live wallet.
narrationstringOptional transfer note.
Response · 200
{  "success": true,  "data": {    "reference": "wd_687d9956569f651f3f3f1894_1753059400000",    "amount": 10000,    "total": 10175,    "fee": 100,    "baseFee": 50  },  "message": "Payout initiated. Funds placed on hold pending transfer confirmation.",  "requestId": "req_1234567890"}

Funds are held, not yet sent

A successful response means the payout was accepted and the amount plus fee is on hold. total is what leaves your balance. Wait for the transfer confirmation before treating it as delivered.
Response · 400 · insufficient balance
{  "success": false,  "message": "Insufficient balance. Required: 10175 (amount: 10000 + fee: 100), Available: 2000",  "requestId": "req_1234567890"}

Virtual cards

Issue virtual cards for online payments. A card is denominated in the currency of its funding wallet and can be frozen or unfrozen. Only masked details are returned — the sensitive payload is decrypted server-side and never stored in the clear.

GET/business/:id/cardsOwner
POST/business/:id/cardsOwner
POST/business/:id/cards/:cardId/freezeOwner
POST/business/:id/cards/:cardId/unfreezeOwner

Owner-authenticated, not API key

Card management runs through the dashboard's owner authentication rather than the API-key surface. Subscribe to card.created if your server needs to react to new cards.

Webhooks

Webhooks are signed HTTP callbacks NorthPole sends when events happen. You choose which events to receive, and configure the endpoint from the dashboard.

POST /business/:id/webhooks · Request
{  "url": "https://your-server.com/webhooks/northpole",  "events": ["wallet.funded", "payment_session.completed", "payment_session.created"],  "secret": "your_webhook_secret"}

Request fields

FieldTypeDescription
urlrequiredstringHTTPS endpoint that will receive payloads.
eventsrequiredstring[]Event types to subscribe to.
secretrequiredstringUsed to derive the HMAC signature you verify against.

Return 2xx within 30 seconds

Any other status, or a slower response, counts as a delivery failure and starts the retry schedule.

Event payloads

Every payload shares the same envelope: event, event_id, data, created_at and delivery_id.

wallet.funded

A business wallet receives a credit — a bank transfer settlement or a completed payment session.

wallet.funded
{  "event": "wallet.funded",  "event_id": "wallet.funded_1712345678_a1b2c3",  "data": {    "transactionId": "TXN_abc123",    "walletId": "wallet_123",    "amount": 5000,    "currency": "NGN",    "businessId": "business_abc"  },  "created_at": "2026-08-25T11:45:00Z",  "delivery_id": "del_1712345678_A1B2C3"}

payment_session.created

A session is created and a dynamic virtual account is assigned. Carries the checkout URL so you can notify the customer immediately.

payment_session.created
{  "event": "payment_session.created",  "event_id": "ps_created_1712345678_a1b2c3",  "data": {    "sessionId": "ps_abc123def456",    "checkout_url": "https://checkout.northpoletradelink.com/checkout/ps_abc123def456",    "businessId": "business_abc",    "amount": 5000,    "currency": "NGN",    "customer_name": "John Doe",    "customer_email": "john@example.com",    "status": "pending",    "expires_at": "2026-08-25T12:00:00Z",    "payment_details": {      "account_number": "1234567890",      "bank_name": "Wema Bank",      "account_name": "Acme Corp"    }  },  "created_at": "2026-08-25T11:00:00Z",  "delivery_id": "del_1712340000_X1Y2Z3"}

payment_session.completed

A session is marked completed after a successful transfer match.

payment_session.completed
{  "event": "payment_session.completed",  "event_id": "ps_completed_1712345678_d4e5f6",  "data": {    "sessionId": "ps_abc123def456",    "payscribe_transaction_id": "PSTXN_789",    "completed_at": "2026-08-25T11:45:00Z"  },  "created_at": "2026-08-25T11:45:00Z",  "delivery_id": "del_1712345678_G7H8I9"}

payment_session.expired

A session expires without payment.

payment_session.expired
{  "event": "payment_session.expired",  "event_id": "ps_expired_1712345678_j0k1l2",  "data": {    "sessionId": "ps_abc123def456",    "businessId": "business_abc",    "amount": 5000,    "currency": "NGN",    "expires_at": "2026-08-25T12:00:00Z"  },  "created_at": "2026-08-25T12:00:00Z",  "delivery_id": "del_1712347200_M3N4O5"}

transaction.completed

A transaction completes successfully.

transaction.completed
{  "event": "transaction.completed",  "event_id": "transaction.completed_1712345678_p6q7r8",  "data": {    "transactionId": "TXN_abc123",    "type": "deposit",    "amount": 5000,    "currency": "NGN",    "status": "success",    "businessId": "business_abc"  },  "created_at": "2026-08-25T11:45:00Z",  "delivery_id": "del_1712345678_S9T0U1"}

card.created

A virtual card is created. Only masked details are returned — the sensitive payload is decrypted server-side.

card.created
{  "event": "card.created",  "event_id": "card.created_1712345678_v2w3x4",  "data": {    "cardId": "card_abc123",    "userId": "user_xyz",    "businessId": "business_abc",    "last4": "1234",    "currency": "USD"  },  "created_at": "2026-08-25T10:30:00Z",  "delivery_id": "del_1712341800_Y5Z6A7"}

kyb.verified

Business KYB verification is approved. Watch this to know when API keys become available.

kyb.verified
{  "event": "kyb.verified",  "event_id": "kyb.verified_1712345678_b8c9d0",  "data": {    "kybId": "kyb_abc123",    "businessId": "business_abc",    "status": "verified",    "verified_at": "2026-08-25T09:00:00Z"  },  "created_at": "2026-08-25T09:00:00Z",  "delivery_id": "del_1712336400_E1F2G3"}

webhook.test

You trigger a test delivery from the dashboard.

webhook.test
{  "event": "webhook.test",  "event_id": "test_1712345678",  "data": {    "message": "This is a test webhook delivery",    "timestamp": "2026-08-25T10:00:00Z"  },  "created_at": "2026-08-25T10:00:00Z",  "delivery_id": "del_1712336400_H4I5J6"}

Signature verification

Each payload is signed with HMAC-SHA256 using your configured secret. Verify the signature before you act on anything — an unverified webhook is just an HTTP request from a stranger.

Request headers

FieldTypeDescription
X-Northpole-SignaturestringHMAC-SHA256 hex digest of the raw JSON body.
X-Northpole-TimestampstringUnix timestamp when the delivery was created.
X-Northpole-Delivery-IdstringUnique delivery identifier. Use it to deduplicate.
X-Northpole-Event-IdstringUnique event identifier.
Content-Typestringapplication/json
Node.js
const crypto = require('crypto'); function verifyWebhookSignature(payload, signature, secret) {  const expected = crypto    .createHmac('sha256', secret)    .update(JSON.stringify(payload))    .digest('hex');  return crypto.timingSafeEqual(    Buffer.from(signature),    Buffer.from(expected)  );} app.post('/webhooks/northpole', (req, res) => {  const sig = req.headers['x-northpole-signature'];  const secret = process.env.NORTHPOLE_WEBHOOK_SECRET;  if (!verifyWebhookSignature(req.body, sig, secret)) {    return res.status(401).send('Invalid signature');  }  console.log('Event received:', req.body.event);  res.status(200).send('OK');});

Compare in constant time

Use timingSafeEqual rather than ===. A plain string comparison leaks how much of the signature matched, which is enough to forge one given time.

Delivery & retries

Delivery behaviour

FieldTypeDescription
Timeout30sYour endpoint must respond within 30 seconds.
Max retries5After five failures the delivery is abandoned.
Retry schedulebackoff1 min, 5 min, 15 min, 60 min, 60 min.
Final stateexhaustedRecorded after five failed attempts.
User-AgentstringNorthPole-Webhook/1.0

Make handlers idempotent

Retries and duplicate deliveries both happen. Key your processing on delivery_id so the same event landing twice does not credit an order twice.

Errors

Standard error response
{  "success": false,  "message": "Business KYB verification required before creating API keys.",  "requestId": "req_uuid",  "error": {    "code": "KYB_VERIFICATION_REQUIRED"  }}
StatusCodeDescription
400KYB_VERIFICATION_REQUIREDAction requires completed KYB verification.
400INVALID_REQUESTMalformed body or missing required fields.
401UNAUTHORIZEDMissing or invalid API key.
404NOT_FOUNDResource not found.
409CONFLICTDuplicate request, or the resource already exists.
422UNPROCESSABLEBusiness validation failed.
429RATE_LIMITEDToo many requests. Back off and retry.

Need help with a specific failure? Send us the requestId from the response — contact developer support.