Changelog

Changelog

Notable changes to the Swappr API. Dates are when the change reached production.

June 24, 20262026-06-24-currencies

Currency discovery endpoint + cleaner name-enquiry errors + clearer provisioning accounts

A new GET /v1/currencies endpoint lets you discover the currencies the platform supports in your environment. We also cleaned up name-enquiry error messages and made it explicit that a still-provisioning virtual account has no account_number yet.

ADDED

GET /v1/currencies — currency discovery. Returns the currencies active in your environment ({ code, name, symbol, decimals }), ordered with the home market (NGN) first. Read-only — any valid key can call it; there’s no filter param. Use it to discover valid currency values instead of hard-coding a list. See Currencies.

CHANGED

A provisioning virtual account now returns account_number: null. While a freshly-issued customer virtual account has status: "provisioning", account_number is null (the field is still present) — do not surface or fund it. Once status becomes "active", the real number is populated. provider_ref is present throughout for correlation.

FIXED

POST /v1/name-enquiry returns cleaner errors. An unresolvable or provider-edge lookup now returns a clear, merchant-facing message instead of raw upstream text. HTTP status and error codes are unchanged.

June 24, 20262026-06-24-create-recovery

Customer create rolls back cleanly on validation rejection

When POST /v1/customers is rejected at create for an invalid identity field, the half-created record is now discarded — so you fix the field and retry with the same customer_reference. We also corrected the next_action upload paths in the create response to /api/v1/....

CHANGED

A validation rejection at create is now recoverable with the same customer_reference. On a 422 validation_failed, the failed record is rolled back (the 422 detail no longer includes customer_id). Fix the offending field and retry reusing the same customer_reference with a fresh Idempotency-Key — you no longer need a new reference. (Post-submission identity corrections — after a customer is submitted for verification — still require a new customer.)

FIXED

next_action upload paths corrected to /api/v1/.... The create-response next_action steps for individual customers now point at /api/v1/customers/{id}/files and /files/attach.

June 18, 20262026-06-18-phone

Individual customers: phone now required at create

POST /v1/customers now requires phone for type: individual, alongside the existing id_expiry_date, dob, street, city, and state. The banking partner needs it to provision a virtual account, so we now reject early with a clear 400 instead of failing later at virtual-account creation.

CHANGED

phone is now required for type: individual. Omitting it returns 400 missing_field (field phone). It was previously optional but is needed for virtual-account provisioning. Presence is required; the phone format is not enforced. Business (type: business) customers are unaffected.

Backwards compatibility

  • Individual customers were already unable to get a virtual account without a phone (the failure just surfaced later, at VBA creation). Requiring it up front turns that into a clear, early error — it doesn’t remove any working flow.
  • Business (type: business) customers are unaffected.
June 18, 20262026-06-18-request-id

Every response carries an X-Request-Id for support tracing

Every API response now documents an X-Request-Id header identifying that exact request — quote it when contacting support so we can trace the precise call.

ADDED

X-Request-Id on every response. Each response (success or error) includes a unique request id. Include it when you contact support and we can trace the exact request — method, path, status, latency, and masked request/response bodies — in our logs. API requests are retained ~13 months for troubleshooting; secrets such as your API key are never stored.

June 18, 20262026-06-18-doc-record

Document uploads recorded + create response signals the next step

POST /v1/customers now returns a next_action and requirements hint for individual customers that are pending verification, so it’s clear an identity document must be uploaded. We also now keep a record of every document uploaded through the API.

ADDED

next_action / requirements on create. For type: individual customers returned as pending, the create response includes next_action: "upload_identity_document" plus a requirements object listing the upload steps. Purely additive — existing fields are unchanged.

ADDED

Document upload records. Uploading an identity document (/files/files/attach) is now recorded against the customer. No request/response shape changed.

June 17, 20262026-06-17-fields

Individual customers: address + DOB now required at create

POST /v1/customers now requires dob, street, city, and state for type: individual, alongside the existing id_expiry_date. The banking partner needs them to provision a virtual account and they cannot be added after the customer is created — so we now reject early with a clear 400 instead of failing later at virtual-account creation.

CHANGED

dob, street, city, state are now required for type: individual. Omitting any returns 400 missing_field (with the field name); a malformed dob returns 400 invalid_field. These were previously optional but are mandatory for virtual-account provisioning and are not updatable after create. id_issue_date remains optional.

Backwards compatibility

  • Individual customers were already unable to get a virtual account without these fields (the failure just surfaced later, at VBA creation, and was unrecoverable because the record can’t be edited after create). Requiring them up front turns that into a clear, early error — it doesn’t remove any working flow.
  • Business (type: business) customers are unaffected.
June 17, 20262026-06-17

KYC file upload returns the Content-Type to send

POST /v1/customers/{id}/files now takes a content_type and returns it inside required_headers as the Content-Type you echo on the upload PUT — so identity documents attach reliably.

CHANGED

POST /v1/customers/{id}/files now requires content_type. Pass the MIME type of the file you’ll upload — one of image/jpeg, image/png, image/webp, image/heic, image/heif, application/pdf. It’s returned inside required_headers as Content-Type. Omitting it returns 400 missing_field; an unsupported value returns 400 invalid_field.

FIXED

Identity documents now attach reliably. Previously the presigned upload’s required_headers carried no Content-Type, so an upload that echoed them verbatim was stored without a valid content type and POST /v1/customers/{id}/files/attach rejected it. The response now always includes the Content-Type for your file.

CHANGED

Docs clarify the byte-PUT is server-side. The signed upload_url is not browser-CORS-enabled — run step 2 from your backend. Requesting the URL and attaching the file are ordinary Bearer-authenticated calls.

Backwards compatibility

  • The previous documented flow (echo required_headers verbatim, no content_type) did not actually complete — attach rejected the file — so adding the required field unblocks the happy path rather than breaking a working one.
  • If you were already setting Content-Type manually on the PUT, you can keep doing so, or drop it and just echo required_headers now that they include it; either way, add content_type to the /files request.
June 15, 20262026-06-15

Stricter request validation + webhook endpoint hardening

This release tightens input validation on list and report endpoints and hardens webhook endpoint registration. Most changes surface a clearer 400 where invalid input was previously accepted silently — valid requests are unaffected.

CHANGED

Invalid filter values now return 400 instead of being silently ignored. GET /v1/beneficiaries?currency= and GET /v1/webhook_deliveries?event_type= return 400 invalid_filter on an unrecognized value, and GET /v1/reports/*?format= returns 400 unsupported_format for anything other than json / csv / xlsx. Previously an unrecognized value was dropped (returning unfiltered, or default-format, results).

CHANGED

GET /v1/reports/bank-statement now requires currency. A bank statement is single-currency — a running balance across mixed currencies is meaningless — so there is no default. Omitting it returns 400 missing_currency; an unrecognized value returns 400 invalid_filter. Pass one of NGN / GBP / USD / EUR / CAD. (Previously defaulted to NGN.)

CHANGED

Webhook endpoint URLs must be publicly reachable. POST and PATCH /v1/webhook_endpoints require an HTTPS URL that resolves to a public address — loopback, private-network, and link-local hosts are rejected with 400 invalid_url. Replaying a delivery to an endpoint whose URL no longer resolves publicly is rejected the same way. Use a public HTTPS tunnel (not raw localhost) when testing locally.

CHANGED

POST /v1/webhook_endpoints now takes an Idempotency-Key. A same-key + same-body retry returns the original endpoint (including its secret) with 200; a same key with a different body returns 409 idempotency_key_conflict. Send a UUID per logical create.

FIXED

Webhook test events now sign with the production scheme. The “send test event” action previously used a legacy signature header format; it now signs with the same X-Swappr-Signature: t=<unix>,v1=<hmac> scheme as real deliveries, so a receiver built against the test event verifies live traffic unchanged. See Webhooks → verify the signature.

Backwards compatibility

  • Valid requests are unaffected. The new 400s only fire on input that was previously accepted-and-ignored (a bad currency / event_type / format), or on a bank-statement call that relied on the NGN default.
  • If you relied on the bank-statement NGN default, add an explicit ?currency= to those calls.
  • If you registered a non-public webhook URL (e.g. a raw localhost), switch to a public HTTPS endpoint or tunnel.
June 8, 20262026-06-08

Individual vs business FX flows de-conflated

The banking partner has two distinct customer types — individual and business — with separate onboarding, KYC, collections, and payouts. We’ve de-conflated the FX gates so the individual-sender flow no longer requires a business/merchant international account. See Individual vs business.

CHANGED

id_expiry_date is now required for type: individual on POST /v1/customers (YYYY-MM-DD, future date). The banking partner needs it to provision the customer’s virtual account, and it cannot be added after verification. Creates without it are rejected with missing_field (field id_expiry_date).

CHANGED

GBP/USD/EUR payout gate is now per-flow. An individual sender (sender_customer_id) sends from the sender’s own virtual account — a new sender_account_not_provisioned (422) is returned if that customer has no active VBA in the currency. A business/treasury sender keeps the existing no_active_international_account (403) gate against the merchant’s international account. CAD routes via Interac for both.

CHANGED

POST /v1/beneficiaries no longer requires an active international account for GBP/USD/EUR — only the international-accounts feature flag (fx_features_not_enabled if off). A beneficiary is a sender-agnostic saved recipient; the capability check now lives at payout time. Previously this returned no_active_international_account at save time.

CHANGED

POST /v1/customers/{id}/kyc is documented as an acknowledgement for individuals — verification runs automatically once identity documents are attached (no separate submit). Status also refreshes from the partner on read.

These changes are additive / unblocking for existing integrators: the new id_expiry_date requirement only affects new individual creates, and the relaxed beneficiary gate only removes a rejection.

June 6, 20262026-06-06

Customer attribution & FX recipients

This release adds end-to-end customer attribution (inflows + outflows tied to a verified end-user), FX beneficiary support, and standardized payout webhooks.

ADDED

Customers APIPOST /v1/customers plus KYC submission, document upload, international account (VIBAN) issuance, and a unified GET /v1/customers/{id}/transactions history. See the Customers reference.

ADDED

FX beneficiariesPOST /v1/beneficiaries now accepts GBP / USD / EUR recipients via a nested bank + address envelope, with an optional external_reference. See the FX shape.

ADDED

FX payoutsPOST /v1/payouts accepts beneficiary_id, customer_id, and customer_reference. The generic customer_id and the FX-specific sender_customer_id both attribute a payout to the same customer; pass either (or both, if they agree).

ADDED

FX gating error codePOST /v1/beneficiaries and POST /v1/payouts report no_active_international_account when an FX (GBP / USD / EUR) operation is attempted without an active international account in that currency.

CHANGED

Standardized payout webhookspayout.processing / payout.paid / payout.failed / payout.reversed now fire on every real status transition (not only on admin force-close), with a consistent payload that includes a customer block when attributed. See Webhooks → Payout lifecycle payload.

ADDED

wallet_funded customer block — VA-credit webhooks for customer-owned accounts now carry a customer block.

ADDED

Per-customer wallet filterGET /v1/wallets/{id}/transactions?customer_id= returns the raw ledger view of one customer’s movements (requires the customer_transaction_view permission).

Backwards compatibility

  • NGN and CAD beneficiaries are unchanged — the existing flat NGN shape and the CAD Interac shape continue to work exactly as before.
  • Existing payout requests are unchangedbeneficiary_id / customer_id / customer_reference are all optional additions. Non-FX integrations need no changes.
  • Idempotency contract preservedPOST /v1/beneficiaries still returns 200 with created: false on an idempotent collision (FX rows dedup the same way as NGN). No 409.
  • Webhook payloads are additive — the customer block is omitted (not null) when a credit/payout isn’t customer-attributed, so existing parsers are unaffected.

Deprecated

  • Inline FX recipient details on POST /v1/payouts are soft-deprecated in favor of referencing a saved beneficiary by beneficiary_id. Inline FX continues to work for now; a future release will announce a hard-deprecation window before removing it. Migrate FX payouts to create a beneficiary first, then pass beneficiary_id.

Migration notes

  1. FX beneficiaries: if you build FX payouts inline today, start creating beneficiaries via POST /v1/beneficiaries (GBP example uses bank.sort_code like "200000") and pass beneficiary_id on the payout.
  2. Customer transactions: grant the customer_transaction_view permission to any API key that needs GET /v1/customers/{id}/transactions or the ?customer_id= wallet filter — without it those calls return 403.
  3. Webhooks: route payout events on the dotted event value (payout.paid) in the body, or the underscore form (payout_paid) in the X-Swappr-Event header — pick one consistently. Keep handlers idempotent (key payouts on reference).