Developer docs
API reference
Conventions
https://api.northpoletradelink.comAmounts
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.
{ "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
/api/business/detailsx-api-keyReturns public-facing details for the business the key belongs to. The business is resolved from the key, so there is no id to pass.
{ "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
| Field | Type | Description |
|---|---|---|
id | string | Business identifier. |
name | string | Registered business name. |
logo | string | null | Logo URL, if uploaded. |
cover_photo | string | null | Cover image URL, if uploaded. |
kyb_status | string | Verification state. Only a verified business can transact. |
is_active | boolean | Whether the business is currently enabled. |
Wallets
/api/business/walletsx-api-keyLists every wallet the business holds, with its virtual account where one has been issued. Each currency gets its own wallet.
[ { "wallet_id": "wallet_123", "currency": "NGN", "balance": 150000, "virtual_account": { "account_number": "1234567890", "bank_name": "Wema Bank", "account_name": "Acme Corp" } }]How funding works
- 01A customer transfers to your virtual account number.
- 02The banking partner detects the incoming transfer and notifies NorthPole.
- 03Your wallet is credited automatically.
- 04A
wallet.fundedwebhook 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
Create a session
/api/business/payments/sessionx-api-key{ "amount": 5000, "currency": "NGN", "customer_name": "John Doe", "customer_email": "john@example.com", "customer_phone": "+2348012345678", "metadata": { "order_id": "ORD-12345" }}Request fields
| Field | Type | Description |
|---|---|---|
amountrequired | number | Decimal major units. Minimum 0.01. |
currencyrequired | "NGN" | NGN only for payment sessions. |
customer_namerequired | string | Customer's full name. |
customer_emailrequired | string | Must be a valid email address. |
customer_phone | string | Required for live sessions, which create a dynamic virtual account. |
payment_reference | string | Optional custom reference used when creating the dynamic virtual account. |
metadata | object | Arbitrary data echoed back on retrieval. |
{ "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
/api/business/payments/session/:sessionIdPublicDeliberately 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.
{ "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
/api/business/payments/session/:sessionId/verifyx-api-keyConfirms the session belongs to your business and reports whether it was paid. This is the call to gate fulfilment on.
{ "verified": true, "session_id": "ps_abc123def456", "status": "completed", "amount": 5000, "paid": true, "completed_at": "2026-08-25T11:45:00Z"}Settle a test session
/api/business/payments/session/:sessionId/settle-testPublic · test onlyCompletes 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.
{ "session_id": "ps_abc123def456", "status": "completed", "environment": "test", "transaction_reference": "test_settle_ps_abc123def456_1712345678"}Session statuses
Status values
| Field | Type | Description |
|---|---|---|
pending | string | Created, awaiting payment. |
completed | string | Payment received and matched. |
expired | string | The hour elapsed without payment. |
Customers
/api/business/customersx-api-keyCreates a customer record under your business. Identity fields are required because the customer is verified on creation.
{ "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
| Field | Type | Description |
|---|---|---|
passwordrequired | string | Minimum 6 characters. |
dobrequired | string | Format YYYY-MM-DD. |
identification_typerequired | enum | BVN, NIN, PASSPORT, DRIVERS_LICENSE or VOTERS_CARD. |
addressrequired | object | street, city, state, country and postal_code are all required. |
identityrequired | object | type, number, country and image are all required. |
Payouts
Live mode only
400 with an explanatory message. USD wallets cannot be withdrawn directly — use an FX conversion first.List payout banks
/api/business/payout/banksx-api-key{ "success": true, "data": [ { "name": "Guaranty Trust Bank", "code": "058" }, { "name": "Access Bank", "code": "044" } ], "requestId": "req_1234567890"}Resolve an account name
/api/business/payout/account-lookupx-api-key{ "bankCode": "058", "accountNumber": "0123456789"}{ "success": true, "data": { "bankCode": "058", "bankName": "Guaranty Trust Bank", "accountNumber": "0123456789", "accountName": "JOHN DOE" }, "requestId": "req_1234567890"}Confirm before you send
accountName to whoever authorises the payout. It is the last cheap moment to catch a wrong account number.Initiate a payout
/api/business/payout/transferx-api-key · live{ "walletId": "507f1f77bcf86cd799439011", "bankCode": "058", "accountNumber": "0123456789", "amount": 10000, "narration": "Supplier payment"}Request fields
| Field | Type | Description |
|---|---|---|
amountrequired | number | Must be positive. Decimal major units. |
bankCoderequired | string | From the bank list. |
accountNumberrequired | string | Recipient account number. |
walletId | string | Source wallet. Defaults to the business's live wallet. |
narration | string | Optional transfer note. |
{ "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
total is what leaves your balance. Wait for the transfer confirmation before treating it as delivered.{ "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.
/business/:id/cardsOwner/business/:id/cardsOwner/business/:id/cards/:cardId/freezeOwner/business/:id/cards/:cardId/unfreezeOwnerOwner-authenticated, not API key
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.
{ "url": "https://your-server.com/webhooks/northpole", "events": ["wallet.funded", "payment_session.completed", "payment_session.created"], "secret": "your_webhook_secret"}Request fields
| Field | Type | Description |
|---|---|---|
urlrequired | string | HTTPS endpoint that will receive payloads. |
eventsrequired | string[] | Event types to subscribe to. |
secretrequired | string | Used to derive the HMAC signature you verify against. |
Return 2xx within 30 seconds
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.
{ "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.
{ "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.
{ "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.
{ "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.
{ "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.
{ "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.
{ "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.
{ "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
| Field | Type | Description |
|---|---|---|
X-Northpole-Signature | string | HMAC-SHA256 hex digest of the raw JSON body. |
X-Northpole-Timestamp | string | Unix timestamp when the delivery was created. |
X-Northpole-Delivery-Id | string | Unique delivery identifier. Use it to deduplicate. |
X-Northpole-Event-Id | string | Unique event identifier. |
Content-Type | string | application/json |
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
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
| Field | Type | Description |
|---|---|---|
Timeout | 30s | Your endpoint must respond within 30 seconds. |
Max retries | 5 | After five failures the delivery is abandoned. |
Retry schedule | backoff | 1 min, 5 min, 15 min, 60 min, 60 min. |
Final state | exhausted | Recorded after five failed attempts. |
User-Agent | string | NorthPole-Webhook/1.0 |
Make handlers idempotent
delivery_id so the same event landing twice does not credit an order twice.Errors
{ "success": false, "message": "Business KYB verification required before creating API keys.", "requestId": "req_uuid", "error": { "code": "KYB_VERIFICATION_REQUIRED" }}| Status | Code | Description |
|---|---|---|
400 | KYB_VERIFICATION_REQUIRED | Action requires completed KYB verification. |
400 | INVALID_REQUEST | Malformed body or missing required fields. |
401 | UNAUTHORIZED | Missing or invalid API key. |
404 | NOT_FOUND | Resource not found. |
409 | CONFLICT | Duplicate request, or the resource already exists. |
422 | UNPROCESSABLE | Business validation failed. |
429 | RATE_LIMITED | Too many requests. Back off and retry. |
Need help with a specific failure? Send us the requestId from the response — contact developer support.
