GuidesPostman quickstart

Postman quickstart — sandbox payout end-to-end

This guide walks through proving the payout API works using Twinqle (the flagship sandbox merchant) + Postman. ~10 minutes start to finish. No real money moves — sandbox uses simulated providers.

By the end you’ll have:

  • A working Postman collection with 8 requests
  • A successful sandbox payout (po_xxx with status: paid)
  • Confidence that auth + idempotency + NUBAN resolve + dispatch all work end-to-end

Prerequisites

Sandbox merchant access

You need a logged-in admin user on Twinqle (or any sandbox merchant). Sign in at the dashboard, switch to Sandbox env via the env switcher pill (top right).

Generate a sandbox API key

Dashboard → API & Webhooks+ Generate new key.

  • Pick Sandbox
  • Label it (e.g. “Postman PoC”)
  • IP allowlist: add your current public IP. Mandatory — without at least one IP, every request returns 403 ip_not_allowed. Look up your IP at ifconfig.me or click “Add my current IP” in the dialog.
  • Click Generate — the secret shows ONCE (sk_test_...). Copy it.

Confirm Twinqle has an NGN wallet

Dashboard → Wallets. You should see an NGN wallet with non-zero balance. If it’s empty, click Simulate inbound funding to credit ₦100,000 (sandbox only).

Postman setup

Create a new Postman environment

Postman → Environments+ New environment → name it Swappr Sandbox. Add three variables:

VariableInitial valueCurrent value
swappr_basehttps://api.swappr.me(same)
api_key(paste your sk_test_... here)(same)
idempotency_key(leave blank)(same — populated by the pre-request script below)

For local dev with the Cloudflare tunnel: replace swappr_base with https://swappr-dev.the-technest.dev.

Add a pre-request script for idempotency

Open your Swappr Sandbox environment → Pre-request Script tab → paste:

// Auto-generate a fresh Idempotency-Key for each request that needs one.
// Postman has a built-in `pm.variables.replaceIn('{{$guid}}')` helper.
pm.environment.set('idempotency_key', pm.variables.replaceIn('{{$guid}}'));

This rotates the idempotency-key on every send so your retries don’t collide with previous requests. (For a real integration you’d persist the same key across retries of the same logical operation — see Idempotency — but for testing, a fresh UUID per send is what you want.)

Create a new collection

Postman → Collections+ → name it Swappr Sandbox PoC. On the collection’s Authorization tab, set:

  • Type: Bearer Token
  • Token: {{api_key}}

This applies the auth header to every request in the collection — no need to set it per-request.

The 8 requests

Add each request below to your Swappr Sandbox PoC collection. Run them in order to walk through the full flow.

1. Auth check — list balances

Confirms your key works + IP is allowlisted.

GET {{swappr_base}}/v1/balances

Expected: 200 OK with a list of your wallets. If you get 403 ip_not_allowed, the request IP doesn’t match the allowlist — check your IP at ifconfig.me + update the key in dashboard.

If you get 401 invalid_api_key, the secret is wrong. Re-paste from your dashboard.

2. List wallets — find the NGN wallet ID

GET {{swappr_base}}/v1/wallets

Look at the response → find the entry with currency: "NGN". Copy its id (starts with ckwallet_ or similar).

Optional: save the wallet id as an env var. In the response panel → click the “Tests” tab on the request → paste:

const json = pm.response.json();
const ngn = json.data.find((w) => w.currency === 'NGN');
if (ngn) pm.environment.set('wallet_id_ngn', ngn.id);

This auto-stamps wallet_id_ngn for use in later requests.

3. Verify a recipient — name enquiry

Look up the bank-of-record name for an account. Useful before creating a payout to confirm the recipient details are correct.

POST {{swappr_base}}/v1/name-enquiry
Idempotency-Key: {{idempotency_key}}
Content-Type: application/json

{
  "account_number": "0690000032",
  "bank_code": "044"
}

Expected: 200 OK with resolved_name (the bank-of-record name from GTBank in this case — sandbox uses Monnify’s resolver by default).

Account 0690000032 at bank 044 (GTBank) is a known-good sandbox test account. Other valid 10-digit NUBAN numbers also work; sandbox provider returns synthetic names.

4. Create a payout

Sandbox payouts complete instantly with simulated provider success.

POST {{swappr_base}}/v1/payouts
Idempotency-Key: {{idempotency_key}}
Content-Type: application/json

{
  "amount_minor": "5000",
  "currency": "NGN",
  "recipient": {
    "account_number": "0690000032",
    "bank_code": "044"
  },
  "merchant_reference": "POSTMAN_TEST_001",
  "narration": "Postman quickstart test"
}

Notice: no recipient.name — sandbox auto-resolves it via NUBAN cascade (same Monnify resolver as step 3). The response will include the bank-of-record name.

Expected: 201 Created with:

{
  "object": "payout",
  "id": "ck...",
  "reference": "po_xxx",
  "status": "paid",
  "currency": "NGN",
  "amount_minor": "5000",
  "fee_minor": "75",
  "tax_minor": "0",
  "total_debit_minor": "5075",
  "recipient_name": "ADAEZE BLESSING NWAFOR",
  "recipient_account": "0690000032",
  "recipient_bank_code": "044",
  "provider": "monnify",
  "provider_ref": "SIM_xxx",
  "merchant_reference": "POSTMAN_TEST_001",
  "created_at": "2026-05-07T...",
  "completed_at": "2026-05-07T..."
}

Save the payout id for the next step. Add a Tests script:

const payout = pm.response.json();
pm.environment.set('payout_id', payout.id);
pm.environment.set('payout_reference', payout.reference);
 
pm.test('payout was paid', () => {
  pm.expect(payout.status).to.eql('paid');
});

5. Retrieve the payout

Confirms the payout record exists + you can read it back.

GET {{swappr_base}}/v1/payouts/{{payout_reference}}

(Both id and reference work in the path — try both.)

Expected: 200 OK with the full payout object including the timeline timestamps (created_at / queued_at / processing_at / completed_at).

6. Re-query payout status

Forces a status refresh by calling the provider directly. In sandbox this returns the same paid status (idempotent).

POST {{swappr_base}}/v1/payouts/{{payout_id}}/requery

Expected: 200 OK with the updated payout. metadata.lastRequery carries the timestamp + reason tag (MRQS = merchant re-query status).

7. Cancel attempt — should fail

Cancellation only works on draft or queued payouts. A paid payout returns invalid_status — this confirms our error envelope works.

POST {{swappr_base}}/v1/payouts/{{payout_id}}/cancel
Content-Type: application/json

{
  "reason": "Testing the cancel error path"
}

Expected: 400 Bad Request:

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_status",
    "message": "Cannot cancel a paid payout."
  }
}

8. Bulk batch (optional)

Push 3 payouts in a single batch. Same currency + recipient shape as single payouts.

POST {{swappr_base}}/v1/batches
Idempotency-Key: {{idempotency_key}}
Content-Type: application/json

{
  "currency": "NGN",
  "items": [
    {
      "amount_minor": "1000",
      "recipient": { "account_number": "0690000032", "bank_code": "044" },
      "merchant_reference": "POSTMAN_BULK_001"
    },
    {
      "amount_minor": "2000",
      "recipient": { "account_number": "0690000032", "bank_code": "044" },
      "merchant_reference": "POSTMAN_BULK_002"
    },
    {
      "amount_minor": "3000",
      "recipient": { "account_number": "0690000032", "bank_code": "044" },
      "merchant_reference": "POSTMAN_BULK_003"
    }
  ]
}
⚠️

Bulk endpoints require payout_bulk_upload permission AND a non-empty IP allowlist. If your key is Owner-created from step 2, both are satisfied. If you generated a key for a non-Owner user, dashboard → key row → enable “Allow bulk” + ensure IPs set.

Expected: 201 Created with the Batch object. If your dual-control threshold is configured higher than ₦60 (the batch total), it auto-approves to approved status; otherwise it lands in awaiting_approval and a teammate must approve via the dashboard.

Negative tests (proving error handling works)

These deliberately trigger errors to verify your error handler will work in production. Add each as a separate Postman request:

N1. Missing required field

POST {{swappr_base}}/v1/payouts
Idempotency-Key: {{idempotency_key}}

{ "currency": "NGN", "recipient": { "account_number": "0690000032", "bank_code": "044" } }

400 missing_field (amount_minor is required)

N2. Invalid bank code

POST {{swappr_base}}/v1/payouts
Idempotency-Key: {{idempotency_key}}

{
  "amount_minor": "1000",
  "currency": "NGN",
  "recipient": { "account_number": "0000000000", "bank_code": "999" }
}

422 recipient_unresolvable (NUBAN cascade exhausted)

N3. Idempotency-Key replay with different body

Send the same request twice with the SAME Idempotency-Key but DIFFERENT amount.

First request (set idempotency_key to a hardcoded value temporarily):

{ "amount_minor": "1000", "currency": "NGN", "recipient": { "account_number": "0690000032", "bank_code": "044" } }

Second request (same Idempotency-Key value):

{ "amount_minor": "9999", "currency": "NGN", "recipient": { "account_number": "0690000032", "bank_code": "044" } }

409 idempotency_conflict on the second call (same key, body changed).

Send the SAME body twice with the same key → second call returns the cached 201 response (idempotent replay; safe to retry).

N4. Beneficiary cool-down

Run the original step 4 payout again within 5 minutes:

422 beneficiary_cooldown — same recipient paid recently. Pass allow_duplicate: true to bypass.

Troubleshooting

SymptomCauseFix
403 ip_not_allowedRequest IP not in key’s allowlistUpdate IP in dashboard or use a static-egress proxy
401 invalid_api_keyWrong / revoked keyRe-paste from dashboard
403 merchant_suspendedMerchant account suspendedContact support
recipient_unresolvable on a real accountSandbox NUBAN resolver couldn’t find the accountTry a different account; sandbox uses a single resolver (default Monnify)
wallet_not_foundNo NGN wallet for this merchant in sandbox envCheck Wallets page; if no wallet shown, contact support
Payout stuck processingSandbox shouldn’t do this — it always returns paid synchronouslyCheck provider configuration on /admin/providers

What’s next?