# Connecting the Pieces
Source: https://docs.afriex.com/api-reference/connecting-the-pieces
How Customers, Payment Methods, and Transactions link together
## What this guide covers
If you've read the quickstart but aren't sure exactly *which ID goes where*, or why you need to create three different things before money can move, this guide is for you.
The Afriex Business API has three core building blocks. They must be created in order, and each one produces an ID that the next step needs. This guide explains what each building block is, what ID it gives you, and exactly where that ID is used.
***
## The big picture
Think of it like this: a **Customer** is the person your business is acting on behalf of (the sender or originator). A **Payment Method** holds the account details of the person being paid (the recipient). A **Transaction** is the instruction to move money, and it needs to know *on whose behalf* you are acting and *which recipient account* to use.
Here is how the IDs connect:
```mermaid theme={null}
flowchart TD
A["Step 1: Create Customer
POST /api/v1/customer
─────────────────────────────
✦ Returns: customerId"]
B["Step 2: Create Payment Method
POST /api/v1/payment-method
─────────────────────────────
↳ Input: customerId
✦ Returns: paymentMethodId"]
C["Step 3: Create Transaction
POST /api/v1/transaction
─────────────────────────────
↳ Input: customerId
↳ Input: destinationId = paymentMethodId (WITHDRAW)
↳ Input: sourceId = paymentMethodId (DEPOSIT)
✦ Returns: transactionId"]
A -->|"customerId"| B
B -->|"paymentMethodId"| C
A -->|"customerId"| C
```
### ID quick-reference
| ID field | Belongs to | Used in |
| - | - | - |
| `customerId` | Customer | Payment Method creation, Transaction creation |
| `paymentMethodId` | Payment Method | Transaction as `destinationId` or `sourceId` |
| `destinationId` | Transaction field | Equals a `paymentMethodId` (used in WITHDRAW) |
| `sourceId` | Transaction field | Equals a `paymentMethodId` (used in DEPOSIT) |
| `transactionId` | Transaction | Status lookups, webhook events |
`destinationId` and `sourceId` in a Transaction are both **Payment Method IDs**. They are not special IDs. They are exactly the `paymentMethodId` value returned when you created the Payment Method. This is the single most common point of confusion.
***
## Each building block explained
### Customer
**What it is:** A Customer is the person or business your business is acting on behalf of. They are the originator or sender of the transaction, not the recipient. For example, if your platform processes payroll for a company, each employee whose salary you send out is a Customer. The Customer record stores their identity information (name, email, phone, country) and is used to attribute transactions to the right person in your system.
**What ID it gives you:** `customerId`
**When you use that ID:**
* Pass it as `customerId` when creating a Payment Method (to link the account to this person)
* Pass it as `customerId` when creating a Transaction (to identify whose account is involved)
**Fields to create a Customer:**
| Field | Required | Example |
| - | - | - |
| `fullName` | Yes | `"Amara Osei"` |
| `email` | Yes | `"amara@example.com"` |
| `phone` | Yes | `"+233201234567"` (E.164 format) |
| `countryCode` | Yes | `"GH"` (ISO 3166-1 alpha-2) |
***
### Payment Method
**What it is:** A Payment Method holds the account details of the person being paid or collected from: their bank account, mobile money wallet, or other financial account. It is linked to a Customer (the originator on whose behalf you are acting), but the account details it contains belong to the end recipient. You need a Payment Method before you can send or receive money, because the Transaction needs to know exactly *which account* to credit or debit.
**What ID it gives you:** `paymentMethodId`
**When you use that ID:**
* As `destinationId` in a WITHDRAW transaction (the account the money is sent to)
* As `sourceId` in a DEPOSIT transaction (the account the money is pulled from)
A Payment Method has a `type` field, either `WITHDRAW` (used to send funds to a recipient) or `DEPOSIT` (used to collect funds from a customer). Make sure you create the right type for the transaction you plan to run.
**Supported channels:**
| Channel | Description | Supports | Common use case |
| - | - | - | - |
| `BANK_ACCOUNT` | Local bank transfer | Withdraw | Payout to Nigerian GTBank, Kenyan KCB |
| `MOBILE_MONEY` | Mobile wallet | Deposit & Withdraw | MTN MoMo, M-Pesa, Airtel Money |
| `SWIFT` | International wire transfer | Withdraw | Cross-border payouts to Europe, US |
| `UPI` | Unified Payments Interface | Withdraw | Payouts to India |
| `INTERAC` | Interac e-Transfer | Deposit & Withdraw | Collections and payouts in Canada |
| `WE_CHAT` | WeChat Pay and Alipay | Withdraw | Payouts to China |
| `VIRTUAL_BANK_ACCOUNT` | Afriex-issued virtual account | Deposit | Receive collections in NGN, GHS, KES, and USD |
| `POOL_ACCOUNT` | Shared pool account | Deposit | Shared collection account (production only) |
| `CRYPTO_WALLET` | Crypto wallet | Deposit | USDC / USDT deposits |
#### Virtual bank accounts
`VIRTUAL_BANK_ACCOUNT` is not created through the standard `POST /api/v1/payment-method` endpoint. It has two dedicated endpoints, and they do different things:
* **[Create Virtual Account](/api-reference/endpoint/payment-methods/virtual-account-create)** (`POST /api/v1/payment-method/virtual-account`) creates one. Pass `currency`, optionally `customerId`, and exactly one of `label` or `amount`.
* **[List Virtual Accounts](/api-reference/endpoint/payment-methods/virtual-account)** (`GET /api/v1/payment-method/virtual-account`) is **read-only**. It returns every active account for that customer and currency, and responds `200` with `data: []` when none exist. It does **not** create one.
The `GET` endpoint used to create an account on the fly when none existed. It no longer does. If you are relying on that behavior, calling `GET` now returns an empty list rather than a new account, with no error. Call `POST` to create.
The two account shapes are chosen by which field you pass to `POST`:
* **Static (`label`):** A permanent virtual bank account. The same account number is returned on every subsequent list for that customer and currency, and the customer can fund it at any time for any amount. `label` is one of `SALES`, `OPERATIONS`, `PAYROLL`, `COLLECTIONS`, `VENDOR_PAYMENTS`, `TAX`, `REFUNDS`, `MARKETING`, `TREASURY`, `GENERAL`.
* **Dynamic (`amount`):** An account tied to a specific amount that expires after a set window (the response includes `expiresInMinutes`). Use this when collecting a specific payment within a defined time.
| | Static | Dynamic |
| - | - | - |
| Triggered by | Passing `label` | Passing `amount` |
| Expires | Never | Yes (see `expiresInMinutes` in response) |
| Amount-specific | No | Yes |
When you pass a `customerId`, virtual accounts are currently restricted to **NGN**. Other currencies for a specific customer are rejected with `UNSUPPORTED_VIRTUAL_ACCOUNT_CURRENCY`. There is also a cap on how many accounts a single business-customer-currency combination can hold; exceeding it returns `VIRTUAL_ACCOUNT_LIMIT_REACHED`.
**POOL\_ACCOUNT** returns the business's shared pool account for a country. Requires `country`; pass `customerId` to scope the response to a specific end-user. Use the `reference` on the response to reconcile incoming deposits.
Virtual accounts are for your **end customers**. Link each one to a `customerId` so payments are attributed to the right person.
***
### Transaction
**What it is:** A Transaction is the actual instruction to move money. It references a Customer and (depending on the type) one of their Payment Methods.
**What ID it gives you:** `transactionId`
**When you use that ID:**
* To check the status: `GET /api/v1/transaction/{transactionId}`
* It appears in webhook events so you can match notifications to records in your system
**Three transaction types:**
| Type | What it does | Payment method needed |
| - | - | - |
| `WITHDRAW` | Send money *from* your Afriex wallet *to* a customer's account | `destinationId` (= paymentMethodId) |
| `DEPOSIT` | Pull money *from* a customer's account *into* your Afriex wallet | `sourceId` (= paymentMethodId) |
| `SWAP` | Convert currency *within* your Afriex wallet | None |
**WITHDRAW:** Your business wallet is the implicit source. Afriex debits your wallet automatically. You do not specify a source payment method.
**DEPOSIT:** Your business wallet is the implicit destination. Afriex credits your wallet automatically. You do not specify a destination payment method.
**SWAP:** Both source and destination are your business wallet. No customer or payment method IDs are needed at all.
***
## The 3-step integration sequence
Here is the exact order of operations, with the IDs flowing from one step to the next.
Register the person on whose behalf the transaction is being made (the originator or sender).
```bash theme={null}
curl -X POST https://sandbox.api.afriex.com/api/v1/customer \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"fullName": "Amara Osei",
"email": "amara@example.com",
"phone": "+233201234567",
"countryCode": "GH"
}'
```
Save the `customerId` from the response. You need it in the next step.
```json theme={null}
{
"data": {
"customerId": "69516dd0464b2213bd74cfad",
"name": "Amara Osei",
"email": "amara@example.com",
"phone": "+233201234567",
"countryCode": "GH"
}
}
```
Register the recipient's bank account or mobile wallet (the account the money will be sent to or collected from). Link it to the Customer from Step 1 by passing their `customerId`.
First, look up the correct institution code for the customer's bank or mobile provider:
```bash theme={null}
curl "https://sandbox.api.afriex.com/api/v1/payment-method/institution?channel=MOBILE_MONEY&countryCode=GH" \
-H "x-api-key: YOUR_API_KEY"
```
Then create the payment method using the `institutionCode` and `institutionName` from that response:
```bash theme={null}
curl -X POST https://sandbox.api.afriex.com/api/v1/payment-method \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "MOBILE_MONEY",
"accountName": "Amara Osei",
"accountNumber": "0201234567",
"countryCode": "GH",
"institution": {
"institutionCode": "MTN",
"institutionName": "MTN"
}
}'
```
Save the `paymentMethodId` from the response. This is what goes into `destinationId` (or `sourceId`) in your transaction.
```json theme={null}
{
"data": {
"paymentMethodId": "690df3281c11eea59108fcaf",
"customerId": "69516dd0464b2213bd74cfad",
"channel": "MOBILE_MONEY",
"accountName": "Amara Osei",
"accountNumber": "0201234567",
"countryCode": "GH"
}
}
```
Now create the transaction using both IDs from the previous steps.
* `customerId` = the ID from Step 1
* `destinationId` = the `paymentMethodId` from Step 2 (for a WITHDRAW)
```bash theme={null}
curl -X POST https://sandbox.api.afriex.com/api/v1/transaction \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"sourceAmount": "10",
"destinationAmount": "91.50",
"sourceCurrency": "USD",
"destinationCurrency": "GHS",
"destinationId": "690df3281c11eea59108fcaf",
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440001",
"reference": "ORDER-98765"
}
}'
```
The response includes a `transactionId`. Use it to track status or match incoming webhook events.
***
## Transaction type payloads side by side
```json WITHDRAW: Pay out to a customer's account theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"sourceAmount": "10",
"destinationAmount": "16500",
"sourceCurrency": "USD",
"destinationCurrency": "NGN",
"destinationId": "690df3281c11eea59108fcaf",
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440001",
"reference": "ORDER-001"
}
}
```
```json DEPOSIT: Collect from a customer's account theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "DEPOSIT",
"sourceAmount": "500",
"destinationAmount": "12",
"sourceCurrency": "KES",
"destinationCurrency": "USD",
"sourceId": "690df3281c11eea59108fcc1",
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440002",
"reference": "COLLECTION-001"
}
}
```
```json SWAP: Convert currency in your wallet theme={null}
{
"type": "SWAP",
"sourceAmount": "500",
"sourceCurrency": "USD",
"destinationCurrency": "NGN",
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440003",
"reference": "SWAP-001"
}
}
```
### What changes between each type
| Field | WITHDRAW | DEPOSIT | SWAP |
| - | - | - | - |
| `customerId` | Required | Required | Not required |
| `destinationId` | Required (= paymentMethodId) | Not used | Not used |
| `sourceId` | Not used | Required (= paymentMethodId) | Not used |
| `destinationAmount` | One of `sourceAmount` / `destinationAmount` | One of `sourceAmount` / `destinationAmount` | Optional (API calculates it at live rate) |
| `meta.idempotencyKey` | Required | Required | Required |
| `meta.reference` | Required | Required | Required |
***
## Payment method fields by channel
Different payment channels require different fields. Use the sections below to find the exact payload shape for your channel.
`customerId` is listed below for every channel because that is the usual case, but it is **optional**. Omit it and the payment method is attached to the business owner instead of an end customer. Pass it whenever the method belongs to one of your customers, which is almost always what you want.
Required fields: `customerId`, `channel`, `accountName`, `accountNumber`, `countryCode`, `institution.institutionCode`, `institution.institutionName`
Look up valid institution codes first: [List Institutions](/api-reference/endpoint/payment-methods/institution)
You can also verify the account holder name before creating: [Resolve Payment Method](/api-reference/endpoint/payment-methods/resolve)
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "BANK_ACCOUNT",
"accountName": "John Doe",
"accountNumber": "0123456789",
"countryCode": "NG",
"institution": {
"institutionCode": "058",
"institutionName": "GTBank"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (the mobile number), `countryCode`, `institution.institutionCode`, `institution.institutionName`
Look up valid institution codes for the country: [List Institutions](/api-reference/endpoint/payment-methods/institution)
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "MOBILE_MONEY",
"accountName": "Amara Osei",
"accountNumber": "0201234567",
"countryCode": "GH",
"institution": {
"institutionCode": "MTN",
"institutionName": "MTN"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (IBAN or account number), `countryCode`, `institution.institutionCode` (SWIFT/BIC), `institution.institutionName`, `institution.institutionAddress`, `recipient.recipientAddress`
Use [Institution Codes](/api-reference/endpoint/payment-methods/institution-codes) to resolve a SWIFT code to a bank name.
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "SWIFT",
"accountName": "John Doe",
"accountNumber": "DE89370400440532013000",
"countryCode": "DE",
"institution": {
"institutionCode": "DEUTDEDB",
"institutionName": "Deutsche Bank",
"institutionAddress": "Taunusanlage 12, 60262 Frankfurt am Main, Germany"
},
"recipient": {
"recipientAddress": "Musterstrasse 1, 10115 Berlin, Germany"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (UPI ID / VPA), `countryCode`, `institution.institutionCode` (`"UPI"`), `institution.institutionName` (`"UPI"`), `recipient.recipientPhone`
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "UPI",
"accountName": "Raj Kumar",
"accountNumber": "rajkumar@upi",
"countryCode": "IN",
"institution": {
"institutionCode": "UPI",
"institutionName": "UPI"
},
"recipient": {
"recipientPhone": "+919876543210"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (the email address registered for Interac), `countryCode`, `institution.institutionCode` (`"INTERAC"`), `institution.institutionName` (`"INTERAC"`), `recipient.recipientEmail`
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "INTERAC",
"accountName": "John Doe",
"accountNumber": "john.doe@email.com",
"countryCode": "CA",
"institution": {
"institutionCode": "INTERAC",
"institutionName": "INTERAC"
},
"recipient": {
"recipientEmail": "john.doe@email.com"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (phone number), `countryCode`, `institution.institutionCode` (`"WECHAT"`), `institution.institutionName` (`"WECHAT"`), `recipient.recipientPhone`
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "WE_CHAT",
"accountName": "Zhang Wei",
"accountNumber": "+8613812345678",
"countryCode": "CN",
"institution": {
"institutionCode": "WECHAT",
"institutionName": "WECHAT"
},
"recipient": {
"recipientPhone": "+8613812345678"
}
}
```
Alipay does not have a channel of its own. Create it on the `WE_CHAT` channel and set `institution.institutionName` to `ALIPAY`; sending `"channel": "ALIPAY"` is rejected.
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (Alipay-linked phone number or account ID), `countryCode`, `institution.institutionName`
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "WE_CHAT",
"accountName": "Zhang Wei",
"accountNumber": "+8613812345678",
"countryCode": "CN",
"institution": {
"institutionName": "ALIPAY"
}
}
```
Virtual bank accounts are not created through the standard `POST /api/v1/payment-method` endpoint. Use the dedicated endpoint instead:
```bash theme={null}
POST /api/v1/payment-method/virtual-account # create one
GET /api/v1/payment-method/virtual-account # list existing ones (read-only)
```
`POST` creates the account; `GET` only lists what already exists and returns `data: []` when there is none.
**Two shapes**, determined by which field you pass to `POST`:
| | Static | Dynamic |
| - | - | - |
| Pass | `label` | `amount` |
| Expires | Never | Yes, see `expiresInMinutes` in response |
| Amount-specific | No | Yes |
With a `customerId`, `NGN` and `GHS` are supported. `KES` and `USD` accounts belong to your business, so omit `customerId` to create them.
See [Create Virtual Account](/api-reference/endpoint/payment-methods/virtual-account-create) and [List Virtual Accounts](/api-reference/endpoint/payment-methods/virtual-account) for the full request and response reference.
***
## Institution fields: which one to use and when
The `institution` object has several fields that look similar. Here is exactly what each one is and when you need it.
| Field | What it is | When you need it |
| - | - | - |
| `institutionCode` | Short identifier for the bank or provider (e.g. `"058"` for GTBank Nigeria, `"MTN"` for MTN Ghana) | **Always required when creating a Payment Method** |
| `institutionName` | Human-readable display name (e.g. `"GTBank"`, `"MTN"`) | **Always required when creating a Payment Method** |
| `institutionId` | Afriex's internal database ID for the institution | **Output only. Never pass this as input.** It appears in responses but is not needed for creation. |
| `institutionAddress` | Physical address of the institution | **Only for SWIFT transfers.** Skip it for all other channels. |
### How to find the right institutionCode
1. Call `GET /api/v1/payment-method/institution?channel=BANK_ACCOUNT&countryCode=NG`
2. The response returns a list of institutions, each with `institutionCode` and `institutionName`
3. Copy those values directly into your Payment Method creation request
For SWIFT and US routing numbers, use `GET /api/v1/payment-method/institution/codes` to resolve a known SWIFT/BIC code to a bank name.
Cache the institution list per country and channel. It changes infrequently and there is no need to fetch it on every Payment Method creation.
***
## The meta object
Every transaction must include a `meta` object with two required fields. Without them, the API returns a `400` error.
```json theme={null}
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"reference": "ORDER-12345"
}
```
| Field | Required | What it does |
| - | - | - |
| `idempotencyKey` | **Yes** | A unique string you generate per transaction attempt. If a transaction already exists with this key, Afriex rejects the request with `409 DUPLICATE_REQUEST` rather than creating a duplicate. It does **not** replay the original transaction. Use a UUID v4. |
| `reference` | **Yes** | Your own internal ID for this transaction (e.g. order number, payment ID, or invoice reference). Used for reconciliation and returned on the transaction as `merchantReference`. |
| `narration` | No | A human-readable description (e.g. `"Salary payment for March 2025"`). |
| `invoice` | Required for SWIFT withdrawals; otherwise No | An S3 object key for the invoice document. Upload the file via [Generate Upload URL](/api-reference/endpoint/media/generate-upload-url) first, then pass the returned `key` here. The object must already exist in storage when the transaction is created. |
Never reuse an `idempotencyKey` across different transaction attempts. Use a fresh UUID for every new transaction. Reusing a key, or reusing a `reference`, is rejected with `409 DUPLICATE_REQUEST` and the new transaction is not created.
Because a duplicate is an error rather than a replay, **a `409` is not a failure to retry**. It means a transaction for that key already exists. Look it up with [List Transactions](/api-reference/endpoint/transactions/list) filtered by your `merchantReference` to find out what happened to it, instead of retrying with a fresh key and risking a genuine double payment.
***
## Common mistakes
### 1. Using a customerId as destinationId or sourceId
`destinationId` and `sourceId` must be a `paymentMethodId`, not a `customerId`. Both look like hex strings, which makes it easy to mix them up.
```json theme={null}
// Wrong: this is a customerId
"destinationId": "69516dd0464b2213bd74cfad"
// Correct: this is the paymentMethodId from Step 2
"destinationId": "690df3281c11eea59108fcaf"
```
***
### 2. Skipping the 3-step sequence
You cannot create a Transaction without first creating a Customer and a Payment Method. The API returns a `404` or `400` if either does not exist. Always follow: **Customer → Payment Method → Transaction**.
***
### 3. Mixing up DEPOSIT and WITHDRAW fields
* **WITHDRAW** uses `destinationId` (where the money goes)
* **DEPOSIT** uses `sourceId` (where the money comes from)
Passing `destinationId` on a DEPOSIT (or `sourceId` on a WITHDRAW) will cause the API to reject the request.
***
### 4. Assuming SWAP needs a payment method
SWAP does not touch any external account. It converts currency inside your Afriex business wallet. No `customerId`, `destinationId`, or `sourceId` is needed. Only the currency pair and the `meta` object are required.
```json theme={null}
{
"type": "SWAP",
"sourceAmount": "500",
"sourceCurrency": "USD",
"destinationCurrency": "NGN",
"meta": {
"idempotencyKey": "unique-uuid-here",
"reference": "SWAP-001"
}
}
```
***
### 5. Omitting meta.idempotencyKey or meta.reference
Both are required on every transaction. The API returns a `400` if either is missing. Always generate a fresh UUID for `idempotencyKey` and use your own internal order or payment ID as `reference`.
***
### 6. Hardcoding institution codes without looking them up
Do not guess institution codes. Use `GET /api/v1/payment-method/institution?channel=BANK_ACCOUNT&countryCode=NG` to get the correct code for each country and channel. The same bank can have a different code in different countries.
***
### 7. Passing institutionId as input
`institutionId` is an output field. It appears in API responses but you never need to supply it when creating a Payment Method. The fields you pass are `institutionCode` and `institutionName`.
***
## Tracking what happens after you create a transaction
Transactions do not complete instantly. After creation the status will be `PENDING` or `PROCESSING`. Here is how to know when it finishes.
**Testing in sandbox?** Sandbox transactions settle automatically in about 5 minutes. Pick the outcome with the payment method's last four digits (e.g. `0001` for fail, `0003` to hold in `PENDING` until you call [`/simulate`](/api-reference/endpoint/transactions/simulate)); add `SIMULATE_INSTANT` anywhere in `meta.reference` to speed things up to about 30 seconds. Full reference: [Testing in Sandbox](/guides/testing-in-sandbox).
**Option A (recommended): Webhooks**
Subscribe to `TRANSACTION.UPDATED` events. Afriex sends an HTTP POST to your webhook URL with the full transaction payload whenever the transaction's status changes.
This fires on intermediate statuses too, not just terminal ones, so expect several deliveries for a single transaction. Branch on the payload's `status` field to decide whether the transaction is actually finished.
See [Webhook setup](/api-reference/endpoint/webhooks/introduction) for configuration steps and payload examples.
**Option B: Poll the status endpoint**
```bash theme={null}
curl "https://sandbox.api.afriex.com/api/v1/transaction/TRANSACTION_ID" \
-H "x-api-key: YOUR_API_KEY"
```
Prefer webhooks over polling. Polling adds latency and uses API quota. Webhooks notify you the moment the status changes.
### Terminal statuses
| Status | Meaning |
| - | - |
| `SUCCESS` | Transaction finished successfully |
| `FAILED` | Transaction could not be processed. Check `meta.failureReason.code` for a stable `AFX_*` reason (see [Failure reasons](/api-reference/models#failure-reasons)). |
| `CANCELLED` | Cancelled before processing started |
| `REFUNDED` | Funds returned to the sender |
| `REJECTED` | Rejected after manual review |
***
## Next steps
Make your first API call in under 5 minutes.
Full parameter reference for customer creation.
All channels and required fields in the API reference.
Full transaction creation reference with all fields.
Complete field-by-field documentation for all models.
Receive real-time transaction status updates.
# Get Balance
Source: https://docs.afriex.com/api-reference/endpoint/balance/get
GET /api/v1/org/balance
Returns the business's **Payout (Withdraw) wallet** balances in the provided currencies, as a flat currency-to-amount map. Balances held in other wallets (for example the Collection wallet that `settlement: "request"` withdrawals debit, and that pool-account deposits credit) are not included in this response and have no endpoint of their own yet.
Returns the wallet balances for your business in the specified currencies.
# Top Up Sandbox Balance
Source: https://docs.afriex.com/api-reference/endpoint/balance/topup
POST /api/v1/org/balance/topup
Credits the business wallet with the specified amount and currency. **This endpoint is only available in the sandbox/staging environment and will return a `403 Forbidden` response in production.** Use it during integration development to top up your test balance without making real transfers.
Credits the business wallet with the specified amount and currency.
This endpoint is only available in the **sandbox/staging** environment and will return a `403 Forbidden` response in production. Use it during integration development to top up your test balance without making real transfers.
# Create Checkout Session
Source: https://docs.afriex.com/api-reference/endpoint/checkout-sessions/create
POST /api/v1/checkout-session
Creates a hosted checkout session for a customer and returns a `checkoutUrl` that the customer should be redirected to in order to complete payment. The session captures the merchant intent (amount, currency, merchant reference, customer details, and allowed payment channels) and is identified end-to-end by the merchant-supplied `merchantReference`.
**Every `/api/v1/checkout-session` route is only available in the sandbox/staging environment and returns a `403 Forbidden` response in production.** You can build and test the full flow against sandbox today; production access is enabled separately once checkout is signed off.
Create a hosted checkout session for a customer. Afriex returns a `checkoutUrl` that you redirect the customer to so they can complete payment on Afriex's hosted page.
Want to see the hosted checkout end to end before you write any code? Try the [sandbox demo](https://sandbox.pay.afriex.com/demo), pick a currency and amount, and click **Proceed to payment**, no API key required.
**Testing in sandbox?** A checkout session's underlying transaction settles automatically in about 5 minutes and triggers the same webhook and redirect behavior as a real payment (see **How it works** below). Pick the outcome with the payer's last four digits (e.g. `0001` fails, `0002` triggers the mobile-money OTP page, `0003` stays pending until you call [`/simulate`](/api-reference/endpoint/transactions/simulate)); add `SIMULATE_INSTANT` anywhere in `merchantReference` to settle in about 30 seconds. Sandbox sessions also enable in-page test-mode helpers (test-number hints, OTP hint, simulate-transfer button, pre-filled test card) so payers can drive outcomes without leaving the page. Full reference: [Testing in Sandbox](/guides/testing-in-sandbox#hosted-checkout-in-sandbox).
## How it works
1. Call this endpoint with the amount you want to collect, the customer's details, your `merchantReference`, and a `redirectUrl`.
2. Afriex returns a `checkoutUrl`.
3. Redirect the customer to that URL.
4. After payment, Afriex redirects the customer back to your `redirectUrl` and fires a `CHECKOUT_SESSION.CREATED` [webhook](/api-reference/endpoint/webhooks/introduction) to your configured callback URL.
## Identifying the session
The `merchantReference` you supply is the end-to-end identifier. Use it to look up the session, match webhook deliveries, and reconcile any resulting transaction in your system. It must be unique per session.
## Amounts are in minor units
`amount` is denominated in the smallest unit of the currency. For example, kobo for `NGN` or cents for `USD`. To charge ₦5,000.00, pass `500000`. Minimum value is `100` (one major unit).
## Choosing payment channels
`channels` is **required** and lists the deposit rails you are willing to offer the customer. Supported values are `VIRTUAL_BANK_ACCOUNT`, `MOBILE_MONEY`, and `CARD`; pass at least one.
**Cards are hosted-checkout only.** `CARD` is available here but not on [Create Payment Method](/api-reference/endpoint/payment-methods/create); to accept card payments, always route them through a checkout session.
**Treat it as a cap, not an exact list.** Channels the session's `currency` cannot collect on are dropped silently, so you can send the same list on every corridor and let Afriex offer the right subset. For example, `["VIRTUAL_BANK_ACCOUNT", "MOBILE_MONEY", "CARD"]` becomes mobile money for a KES session and cards for a USD session, all from one integration.
The response echoes what the payer will actually be shown as `channels`, in the order you sent them.
If none of the requested `channels` are supported for the `currency` (or the currency has no deposit channel at all), the request is rejected with `422 Unprocessable Entity`.
# Checkout Flow Guide
Source: https://docs.afriex.com/api-reference/endpoint/checkout-sessions/user-flow-guide
Step-by-step guide for the hosted Afriex Checkout payment experience
## Overview
Afriex Checkout is a hosted payment experience that allows customers to complete payments using supported methods such as Card, Bank Transfer, and Mobile Money.
After a merchant [creates a checkout session](/api-reference/endpoint/checkout-sessions/create) through the API, the customer is redirected to the hosted checkout page to complete payment.
Walk through the exact flow described below without writing any code. The demo is sandbox-only and uses the real hosted checkout, with all three channels covered per currency: **mobile money** (KES, GHS and others), **bank transfer** (NGN), and **card** (USD).
## Before you begin
A checkout session must already be created by the merchant or application. The hosted checkout page displays the payment amount, available payment methods, session timer, and payment instructions.
## Select a payment method
The customer sees the available payment methods on the checkout page.
Available methods may include:
* Card
* Bank Transfer
* Mobile Money
The customer:
* Reviews the payment amount
* Selects a preferred payment method
The checkout session also displays an expiration timer.
After selecting a method, the customer follows one of the payment paths below.
## Pay by card
When Card is selected, Afriex displays a secure card payment form for the payment.
The page displays:
* Amount to pay
* Card number field
* Card expiry field
* CVC field
* Cardholder name field
* Billing address fields
* Pay button
Customer actions:
1. Enter the card number.
2. Enter the card expiry date and CVC.
3. Enter the cardholder name and billing address.
4. Click **Pay**.
5. Complete any card authorization prompt if required.
Afriex confirms the card payment and updates the checkout page automatically.
## Pay by bank transfer
When Bank Transfer is selected, Afriex displays one-time bank transfer details for the payment.
The page displays:
* Amount to pay
* Bank name
* Account number
* Account name
* Copy account number action
* Payment instructions
* Session expiration timer
Customer actions:
1. Open a banking app, internet banking, or USSD.
2. Transfer the exact amount shown.
3. Return to the checkout page.
4. Wait for automatic payment confirmation.
The customer must send the exact amount shown. Incorrect amounts may cause delays or require a refund.
## Pay by mobile money
When Mobile Money is selected, the customer enters their mobile money details.
The page displays:
* Network selector
* Account holder name field
* Mobile money phone number field
* Pay now button
Customer actions:
1. Select a mobile money network.
2. Enter the account holder name.
3. Enter the mobile money phone number.
4. Click **Pay now**.
Afriex sends a payment authorization prompt to the customer's phone.
### Approve the payment on your phone
After the customer clicks **Pay now**, the checkout page enters a pending state while waiting for payment authorization.
The customer:
* Checks their phone
* Approves the mobile money payment prompt
* Waits for the checkout page to update automatically
The session timer continues while Afriex waits for confirmation from the network or payment provider.
## Payment outcomes
Regardless of the payment method, the checkout session ends in one of two outcomes.
### Successful payment
When payment is confirmed, the customer sees a success message.
At this stage:
* The payment has been confirmed
* The customer sees a success confirmation
* The customer is redirected back to the merchant's redirect URL automatically
### Failed payment
If payment cannot be confirmed, the customer sees a failed payment screen.
Possible failure reasons include:
* Declined or failed card payment
* Incorrect transfer amount
* Declined mobile money authorization
* Expired checkout session
* Network or payment provider issue
* Payment could not be confirmed
The customer may:
* Contact support
* Return to the merchant
* Retry payment if the merchant provides a new checkout session
## Checkout session expiration
Checkout sessions have an expiration timer. Customers should complete payment before the session expires.
If the session expires, the customer may need to restart checkout or request a new checkout session.
## Redirect behavior
After a successful or failed payment, Afriex redirects the customer back to the merchant application depending on the checkout configuration.
To reconcile the payment independently of the redirect, subscribe to [`TRANSACTION.UPDATED`](/api-reference/endpoint/webhooks/transaction-events) on the transaction the session produces. That is the event that reports payment progress and completion.
`CHECKOUT_SESSION.CREATED` is **not** a payment-completion signal. It fires the moment you create the session, in the same call, and carries no information about whether the customer has paid. Do not wait on it to confirm a payment.
## What the payer sees in sandbox
When a checkout session is created with a sandbox API key, the hosted page opens in **test mode** with a few in-page helpers so the payer can drive different outcomes without leaving the flow:
* A **Test mode** badge in the page header.
* On the mobile-money screen, the sandbox **test numbers** are listed inline (`0001` fails, `0002` asks for an OTP, `0005` is declined). Any other valid mobile number succeeds.
* The **OTP hint** (`123456`) is shown whenever an OTP step is triggered.
* On the bank-transfer screen, **Simulate transfer** and **Simulate failed transfer** buttons finalize the deposit without needing to move money from a real bank.
* The card form is pre-filled with a valid **test card** in read-only fields, so a `Pay` click is enough.
These helpers only appear in sandbox; the production checkout looks identical to a real payment.
## Notes for merchants
* Customers should pay the exact amount shown on the checkout page.
* Checkout sessions should be tested in sandbox before going live. See the [Testing in Sandbox guide](/guides/testing-in-sandbox#hosted-checkout-in-sandbox) for the outcome-selection table (last four digits of the payer's account, phone, or card) and how to speed up settlement.
* The redirect URL should be configured correctly so customers return to the merchant application after payment.
## Related
Create a hosted checkout session and get the `checkoutUrl` to redirect your customer to.
Receive real-time notifications when a checkout session is completed.
# Create Customer
Source: https://docs.afriex.com/api-reference/endpoint/customers/create
POST /api/v1/customer
Creates a new customer for a business, with optional KYC information.
Create a new customer for your business. Customers can be created with optional KYC (Know Your Customer) information for enhanced verification.
# Delete Customer
Source: https://docs.afriex.com/api-reference/endpoint/customers/delete
DELETE /api/v1/customer/{customerId}
Deletes a specific customer by their unique identifier.
Delete a customer from your business. This action cannot be undone.
# Get Customer
Source: https://docs.afriex.com/api-reference/endpoint/customers/get
GET /api/v1/customer/{customerId}
Retrieves a specific customer by their unique identifier. The response includes the customer's `reference`, which is the value to supply as the pool-account `reference` when submitting a payment proof.
Retrieve a specific customer by their unique identifier.
# List Customers
Source: https://docs.afriex.com/api-reference/endpoint/customers/list
GET /api/v1/customer
Retrieves a paginated list of customers.
Retrieve a paginated list of all customers associated with your business.
# Update Customer
Source: https://docs.afriex.com/api-reference/endpoint/customers/update
PATCH /api/v1/customer/{customerId}
Partially updates a customer's profile. Send at least one of `fullName`, `email`, or `phone`; omitted fields are left unchanged.
Partially update a customer's profile. Send at least one of `fullName`, `email`, or `phone`; any field you omit is left unchanged.
# Update Customer KYC
Source: https://docs.afriex.com/api-reference/endpoint/customers/update-kyc
PATCH /api/v1/customer/{customerId}/kyc
Partially updates a customer's KYC information.
Update a customer's KYC (Know Your Customer) information. Use this endpoint to add or update verification documents for a customer.
# Verify Customer
Source: https://docs.afriex.com/api-reference/endpoint/customers/verify
POST /api/v1/customer/{customerId}/verify
Runs an identity verification against a customer document. Today the only supported `docType` is `BVN` (Nigeria); `docValue` is the document number. Verification is rate limited per business.
Run an identity verification against a customer document. Today the only supported document type is the Nigerian **BVN**; pass it as `docType: "BVN"` with the number in `docValue`.
Verification is rate limited per business. If you exceed the limit, the endpoint returns `429 Too Many Requests`.
# Generate Upload URL
Source: https://docs.afriex.com/api-reference/endpoint/media/generate-upload-url
POST /api/v1/media/url
Generates a presigned S3 URL that the caller can use to upload a file (e.g. a KYC document) directly to Afriex's secure storage. The returned URL is time-limited and scoped to the requested operation.
Generates a short-lived, presigned S3 URL that you can `PUT` a file to directly. Use it for KYC documents on customers or for transaction-related files such as SWIFT invoices.
Call `POST /api/v1/media/url` with the target `fileName` and a `type` (`user` for identity documents, `transaction` for transaction files). The response contains a presigned `url`, the stable object `key`, and `expiresIn` (seconds the URL remains valid; typically `300`).
Send an HTTP `PUT` to the `url` with the file bytes as the body within the `expiresIn` window. If the window elapses, request a fresh URL.
Attach the returned `key` to any follow-up request (for example, the `invoice` field on a SWIFT withdraw, or a document field in [SME Registration](/api-reference/endpoint/sme-registration/register)). Downstream endpoints only accept the object `key`; the presigned `url` itself is never accepted.
# Add Recipient to Batch
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/add-recipient
POST /api/v1/payment-batch/{batchId}/recipients
Requires the `PAYMENT_METHOD.CREATE` key permission.
Adds a single recipient to a batch. For adding many at once, use [bulk add recipients](/api-reference/endpoint/payment-batches/add-recipients-bulk).
Requires the `PAYMENT_METHOD.CREATE` key permission.
# Bulk Add Recipients to Batch
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/add-recipients-bulk
POST /api/v1/payment-batch/{batchId}/recipients/bulk
Adds each recipient independently and reports which were added. The results are keyed by account number. Requires the `PAYMENT_METHOD.CREATE` key permission.
Adds several recipients to a batch in one call. Each recipient is processed independently, and the response reports which were added successfully, keyed by account number.
Requires the `PAYMENT_METHOD.CREATE` key permission.
# Create Payment Batch
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/create
POST /api/v1/payment-batch
Creates an empty batch. Add recipients with the recipient endpoints, then start the payouts with POST /api/v1/payment-batch/{batchId}/withdraw. Requires the `PAYMENT_METHOD.CREATE` key permission.
Creates an empty payment batch. Add recipients with the [add recipient](/api-reference/endpoint/payment-batches/add-recipient) or [bulk add recipients](/api-reference/endpoint/payment-batches/add-recipients-bulk) endpoints, then start the payouts with [withdraw](/api-reference/endpoint/payment-batches/withdraw).
Requires the `PAYMENT_METHOD.CREATE` key permission.
# Delete Payment Batch
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/delete
DELETE /api/v1/payment-batch/{batchId}
Requires the `PAYMENT_METHOD.CREATE` key permission.
Deletes a payment batch. The saved payment methods for its recipients are kept; only the batch grouping is removed.
Requires the `PAYMENT_METHOD.CREATE` key permission.
# Get Payment Batch
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/get
GET /api/v1/payment-batch/{batchId}
Requires the `PAYMENT_METHOD.READ` key permission.
Returns a single payment batch by id, including its funding method and any last-run outcomes.
Requires the `PAYMENT_METHOD.READ` key permission.
# List Payment Batches
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/list
GET /api/v1/payment-batch
Returns the batches belonging to the calling business. Requires the `PAYMENT_METHOD.READ` key permission.
Returns the payment batches belonging to your business, paginated. Requires the `PAYMENT_METHOD.READ` key permission.
If [request signing](/api-reference/introduction#request-signing) is enabled, sign the query parameters with defaults applied: a call with no parameters signs `{"limit":100,"page":0}`.
# List Batch Recipients
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/list-recipients
GET /api/v1/payment-batch/{batchId}/recipients
Requires the `PAYMENT_METHOD.READ` key permission.
Lists the recipients currently in the batch, with their account details and per-recipient amount.
Requires the `PAYMENT_METHOD.READ` key permission.
# List Batch Sessions
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/list-sessions
GET /api/v1/payment-batch/{batchId}/sessions
Requires the `TRANSACTION.HISTORY.READ` key permission.
Returns the past runs (sessions) of a batch. Each session records one execution of [withdraw](/api-reference/endpoint/payment-batches/withdraw), the recipient outcomes for that run, and its overall status.
Requires the `TRANSACTION.HISTORY.READ` key permission.
# Remove Recipient from Batch
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/remove-recipient
DELETE /api/v1/payment-batch/{batchId}/recipients/{recipientId}
Removes the recipient from this batch only; the saved account is kept. Requires the `PAYMENT_METHOD.CREATE` key permission.
Removes the recipient from this batch only. The saved payment method remains and can still be used outside the batch.
Requires the `PAYMENT_METHOD.CREATE` key permission.
# Update Payment Batch
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/update
PATCH /api/v1/payment-batch/{batchId}
Replaces the batch's name and funding method. Send both: the batch is updated whole. Requires the `PAYMENT_METHOD.CREATE` key permission.
Replaces the batch's name and funding method. Send both fields, the batch is updated whole.
Requires the `PAYMENT_METHOD.CREATE` key permission.
# Update Recipient in Batch
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/update-recipient
PATCH /api/v1/payment-batch/{batchId}/recipients/{recipientId}
Replaces the recipient's account details and amount. Pass `paymentMethodId` to update that saved account in place. Requires the `PAYMENT_METHOD.CREATE` key permission.
Replaces a recipient's account details and amount. Pass `paymentMethodId` to update the saved payment method in place; omit it to leave the account alone and change only the amount.
Requires the `PAYMENT_METHOD.CREATE` key permission.
# Withdraw Batch
Source: https://docs.afriex.com/api-reference/endpoint/payment-batches/withdraw
POST /api/v1/payment-batch/{batchId}/withdraw
Creates a payout for every recipient in the batch in one call and records the run as a session. The payouts settle asynchronously, so a webhook callback URL must be configured. Requires the `TRANSACTION.WITHDRAW.CREATE` key permission.
Each call is a new run and pays every recipient again. To retry only the payouts that failed, pass the run's `sessionId`. A call made while a run of the same batch is still in progress is refused with `409`.
Starts the payouts for a batch: creates a payout transaction for every recipient in one call and records the run as a session. The payouts settle asynchronously.
A **webhook callback URL must be configured** before you call this, otherwise you have no way to observe individual payout outcomes. Subscribe to [Transaction Events](/api-reference/endpoint/webhooks/transaction-events) for the per-recipient status updates, and poll [List Batch Sessions](/api-reference/endpoint/payment-batches/list-sessions) for the overall run.
Passing the previous run's `sessionId` retries only the payouts that failed on that run; calling withdraw while a run is still in progress on the same batch is refused with `409`.
Requires the `TRANSACTION.WITHDRAW.CREATE` key permission.
If [request signing](/api-reference/introduction#request-signing) is enabled, sign the query parameters with defaults applied: a call with no `sessionId` signs `{}`.
# Create Payment Method
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/create
POST /api/v1/payment-method
Creates a new payment method. `accountName`, `accountNumber` and `institution` are required for every channel except UPI and INTERAC, which take no `institution`, and VIRTUAL_BANK_ACCOUNT, which takes none of the three.
Create a new payment method for a customer. Supported channels include bank accounts, mobile money, SWIFT, UPI, Interac, and WeChat.
**Cards are not created here.** `CARD` is a hosted-checkout channel only, available through [Create Checkout Session](/api-reference/endpoint/checkout-sessions/create); this endpoint does not accept it.
> Please note that fields without values are not included in the response. This includes fields with empty strings, null values, and undefined values.
**`accountName` character rules.** For every channel except `WE_CHAT` and `ALIPAY`, the account holder's name must use Latin-script letters (A-Z / a-z, including accented forms such as `é`, `ñ`, `ü`, `ç`) **or Chinese (Han) characters**, digits (0-9), spaces, and the characters `&`, `(`, `)`, `+`, `,`, `-`, `.`, `/`, `:`, `?`, and apostrophe (`'`). At least one letter is required. Names in other scripts (e.g. Arabic, Cyrillic) or containing other symbols are rejected. For `WE_CHAT` and `ALIPAY` payouts, submit the beneficiary's local-script name (e.g. Chinese characters) as it appears on their wallet.
# Get or Create Crypto Wallet
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/crypto-wallet
GET /api/v1/payment-method/crypto-wallet
Retrieves an existing crypto wallet or creates a new one for the specified crypto asset. **Important:** This endpoint is **only active in production** and does **not work on staging/dev**. Supports idempotency to prevent duplicate wallet creation. Currently supports USDT and USDC assets. Send a GET request with the required `asset` parameter (and optional `customerId`). The system will either return existing wallet addresses or automatically create and return new ones if none exist for that asset/customer (or business if no customerId is provided).
Retrieve an existing crypto wallet or create a new one for the specified crypto asset. Supports USDT and USDC assets.
**Important:** This endpoint is **only active in production** and does **not work on staging/dev**.
Want to see this in action? Learn how to build payment links using this endpoint:\
[https://dev.to/afriex/send-a-link-get-paid-building-payment-links-with-afriex-13ak](https://dev.to/afriex/send-a-link-get-paid-building-payment-links-with-afriex-13ak)
# Delete Payment Method
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/delete
DELETE /api/v1/payment-method/{paymentMethodId}
Removes a payment method from the system.
Remove a payment method from the system. This action cannot be undone.
# Get Payment Method
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/get
GET /api/v1/payment-method/{paymentMethodId}
Retrieves a specific payment method by its unique identifier.
Retrieve a specific payment method by its unique identifier.
# List Institutions
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/institution
GET /api/v1/payment-method/institution
Retrieves a list of banks or mobile money providers available for a specific country.
Retrieve a list of banks or mobile money providers available for a specific country and payment channel.
# Institution Codes
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/institution-codes
GET /api/v1/payment-method/institution/codes
Resolves a bank code (SWIFT code or US routing number) to the corresponding bank or institution name.
Resolves a bank code to the institution it belongs to. Give it a SWIFT/BIC code or a US routing number as the `searchTerm`, along with the `codeType`, and it returns the matching bank or institution name.
Use this when you hold a code and need the institution behind it, for example resolving a SWIFT code to a bank name before creating a `SWIFT` payment method.
This is not the same as [Resolve Payment Method](/api-reference/endpoint/payment-methods/resolve), which takes an account number and returns the account holder's details. This endpoint resolves the *institution*, not the account.
# List Payment Methods
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/list
GET /api/v1/payment-method
Retrieves a paginated list of payment methods.
Retrieve a paginated list of all payment methods associated with your business.
# Get Pool Account
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/pool-account
GET /api/v1/payment-method/pool-account
Returns the pool account for the supplied `country`. Use the
`reference` on the response to reconcile incoming deposits. Each call
also sends a `PAYMENT_METHOD.UPDATED` webhook carrying the pool
account.
Returns the business's pool account for a given country. A pool account is a shared, Afriex-managed account that customers pay into; Afriex reconciles incoming funds to the merchant using the `reference` returned on this response.
**Important:** Pool accounts are **only active in production** and do **not work on staging/dev**.
***
## How the call works
1. Call this endpoint with `country` (required). Optionally pass `customerId` to scope the response to a specific end-user.
2. Afriex returns the pool account (account name, account number, institution) as a `PaymentMethod`, along with a `reference`.
3. Share the account details with your customer so they can pay in. When Afriex receives the inbound funds, it uses the `reference` to attribute the deposit on your account.
## The `reference` field
Every response includes a `reference` string. Persist it alongside the deposit you expect and use it to reconcile incoming pool deposits.
When you pass a `customerId`, the response `reference` matches it, so the deposit can be attributed back to that customer on your side.
## Currency is derived from country
Unlike dedicated virtual accounts, this endpoint does not take a `currency` parameter. The currency is determined by the country code (for example, `NG` resolves to `NGN`). Pool accounts are only available in countries where Afriex operates a pool; an unsupported country code returns a `400`.
***
## Related
* [List Virtual Accounts](/api-reference/endpoint/payment-methods/virtual-account) for dedicated per-customer accounts.
* [Create Virtual Account](/api-reference/endpoint/payment-methods/virtual-account-create) to mint a new dedicated account.
# Resolve Payment Method
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/resolve
GET /api/v1/payment-method/resolve
Resolves payment method user information for bank accounts or mobile money.
Resolve payment method user information for bank accounts or mobile money. This endpoint validates and retrieves recipient details based on account number and institution code.
# List Virtual Accounts
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/virtual-account
GET /api/v1/payment-method/virtual-account
Returns every active virtual account for the resolved
customer-or-owner and currency. Read-only: when no virtual account
exists, the response is **200** with `data: []`.
Pass `customerId` to scope the list to a specific end-user; omit it
to list the virtual accounts belonging to the business owner.
**Breaking change from earlier versions:** the previous endpoint
returned a single virtual account and would create one on the fly
when absent. It now returns a list and never creates. The create
path lives at `POST /api/v1/payment-method/virtual-account`; the
`POOL_ACCOUNT` variant has moved to
`GET /api/v1/payment-method/pool-account`.
Returns every active virtual account for the resolved customer-or-owner and currency. The endpoint is read-only: when no virtual account exists, you get a `200` response with `data: []` rather than a `404`.
Pass `customerId` to scope the list to a specific end-user. Omit it to list virtual accounts that belong to the business owner.
Virtual accounts are available in `NGN`, `GHS`, `KES`, and `USD`. See [Create Virtual Account](/api-reference/endpoint/payment-methods/virtual-account-create#supported-currencies) for which currencies your customers can hold and which are business-only. They also work in sandbox, where you fund them with [Simulate Virtual Account Transfer](/api-reference/endpoint/payment-methods/virtual-account-simulate-transfer).
Want to see virtual accounts in action? Learn how to build payment links using them: [https://dev.to/afriex/send-a-link-get-paid-building-payment-links-with-afriex-13ak](https://dev.to/afriex/send-a-link-get-paid-building-payment-links-with-afriex-13ak)
***
## Breaking change from earlier versions
Previously this endpoint returned a single virtual account and would create one on the fly when none existed. It now returns a list and never creates. The behaviour has been split into three explicit endpoints:
| Endpoint | What it does |
| - | - |
| `GET /api/v1/payment-method/virtual-account` | Lists existing virtual accounts (this page). Read-only. |
| `POST /api/v1/payment-method/virtual-account` | Creates a new virtual account. See [Create Virtual Account](/api-reference/endpoint/payment-methods/virtual-account-create). |
| `GET /api/v1/payment-method/pool-account` | Returns the business pool account for a country. Use the `reference` on the response to reconcile incoming deposits. See [Get Pool Account](/api-reference/endpoint/payment-methods/pool-account). |
If you were relying on the get-or-create behaviour, call `GET` first; when `data` is empty, follow up with `POST` to create one.
***
## Virtual accounts are for end customers
Virtual accounts are designed for your **customers as end users**, meaning the people your business is acting on behalf of. Each virtual account is linked to a `customerId` (or to the business owner if no `customerId` is provided). Payments received into a virtual account are attributed to the associated customer in your Afriex system.
Make sure you have created a Customer record before listing or creating virtual accounts for that person. See [Create Customer](/api-reference/endpoint/customers/create).
# Create Virtual Account
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/virtual-account-create
POST /api/v1/payment-method/virtual-account
Creates a new virtual account for the resolved customer-or-owner and
currency. Pass `customerId` to mint the VA for a specific end-user;
omit it to mint one for the business owner.
**Supported currencies.** A virtual account for one of your customers
(`customerId` supplied) can be created in `NGN` or `GHS`. Any other
currency returns `UNSUPPORTED_VIRTUAL_ACCOUNT_CURRENCY`; create it for
the business itself by omitting `customerId`, which is how `KES` and
`USD` accounts are created.
Subject to a per-(business, customer, currency) limit. Requests that
would exceed the limit return `VIRTUAL_ACCOUNT_LIMIT_REACHED`.
**Compliance review:** every currency requires a compliance-approved
application before issuance. The first request for a not-yet-approved
business/currency starts that review and returns
`VIRTUAL_ACCOUNT_PENDING_COMPLIANCE_REVIEW`; retry once approved.
**Accounts opened asynchronously:** for some currencies the issuing
bank opens the account after the request returns. The response is
then `201` with an empty `data` object, and the account is delivered
through the `PAYMENT_METHOD.CREATED` webhook once it is open.
**Label vs amount:** `label` groups static virtual accounts by
purpose (e.g. `SALES`, `OPERATIONS`). It is **mutually exclusive
with `amount`**, which routes to a dynamic (ephemeral) virtual
account. Supplying both is rejected at validation.
Creates a new virtual account for the resolved customer-or-owner and currency. Pass `customerId` to mint the virtual account for a specific end-user, or omit it to mint one for the business owner.
## Supported currencies
You can create virtual accounts in **`NGN`, `GHS`, `KES`, and `USD`**. The currency decides who can hold the account:
| Currency | Who can hold it | Notes |
| - | - | - |
| `NGN` (Nigeria) | Your customers or your business | Static accounts for a customer need a BVN. See [BVN requirement](#bvn-requirement-for-ngn-static-virtual-accounts). |
| `GHS` (Ghana) | Your customers or your business | |
| `KES` (Kenya) | Your business only (omit `customerId`) | To have KES accounts issued under your own legal entity, complete [SME registration](/api-reference/endpoint/sme-registration/register). |
| `USD` | Your business only (omit `customerId`) | |
More currencies are on the way. See [Supported currencies](/api-reference/supported-currencies) for every country and rail, including what is coming soon.
**Customers can hold `NGN` and `GHS` accounts.** Requesting any other currency with a `customerId` returns `UNSUPPORTED_VIRTUAL_ACCOUNT_CURRENCY` with `403`. To create a `KES`, `USD`, or other supported currency account, omit `customerId` so the account is minted for the business itself.
**Testing in sandbox.** You can create virtual accounts in sandbox for the currencies available in production, and no real money moves. A sandbox account receives nothing on its own, so use [Simulate Virtual Account Transfer](/api-reference/endpoint/payment-methods/virtual-account-simulate-transfer) to pay into it. See [Testing in Sandbox](/guides/testing-in-sandbox).
***
## Static vs dynamic virtual accounts
You choose between two flavours of virtual account by what you pass alongside `currency` and `customerId`.
### Static (label)
Pass a `label` to group static virtual accounts by purpose. Static accounts are permanently assigned to the customer and never expire. The same account number remains valid for repeated funding.
Allowed labels: `SALES`, `OPERATIONS`, `PAYROLL`, `COLLECTIONS`, `VENDOR_PAYMENTS`, `TAX`, `REFUNDS`, `MARKETING`, `TREASURY`, `GENERAL`.
Use static accounts when you want stable, long-lived account numbers a customer can top up over time, organised by purpose.
### Dynamic (amount)
Pass an `amount` to mint a dynamic (ephemeral) virtual account tied to that specific amount. Dynamic accounts expire after a short window.
Use dynamic accounts when you need to collect a specific, known amount within a defined window, like a one-time payment for a particular order.
`label` and `amount` are mutually exclusive. Supplying both is rejected at validation.
***
## BVN requirement for NGN static virtual accounts
For NGN static virtual accounts (label-based, Nigeria), the customer **must have a Bank Verification Number (BVN)** on file. This is a regulatory requirement for permanently assigned accounts in Nigeria. If the BVN is missing, the API returns an error.
You can supply a BVN when creating a new customer or add it to an existing customer at any time using the [Update Customer KYC](/api-reference/endpoint/customers/update-kyc) endpoint.
NGN static virtual accounts will fail if the customer has not provided their BVN. This requirement does not apply to dynamic virtual accounts or to virtual accounts in other currencies.
***
## Limits
Creation is subject to a per-(business, customer, currency) cap. Requests that would exceed the cap return `VIRTUAL_ACCOUNT_LIMIT_REACHED` with `400`. Call [List Virtual Accounts](/api-reference/endpoint/payment-methods/virtual-account) first to check what already exists for the customer.
# Simulate Virtual Account Transfer
Source: https://docs.afriex.com/api-reference/endpoint/payment-methods/virtual-account-simulate-transfer
POST /api/v1/payment-method/virtual-account/simulate-transfer
Sandbox only; returns 403 in production. Credits a bank transfer into one of your sandbox virtual accounts, the way the account provider's deposit notification would, so balances and your transaction webhook behave as in production. Pass the account number; for an account created with an `amount` (a one-time account), also pass the `reference` it was issued with. The deposit is reported by the transaction webhook.
**Sandbox only.** Returns `403` in production.
Credits a bank transfer into one of your sandbox virtual accounts, exactly as the underlying provider's deposit notification would in production. Balances and the `TRANSACTION.UPDATED` webhook behave the same.
Sandbox virtual accounts receive no money on their own, so use this endpoint whenever you want to test what happens after a customer funds one.
## Choosing the account
* **Reusable virtual account** (created without `amount`): pass the `accountNumber` and `amount` to credit.
* **One-time virtual account** (created with `amount`): pass the same `reference` the account was issued with, in addition to `accountNumber` and `amount`.
## Controlling the outcome
`outcome` defaults to `success`. Pass `"outcome": "failed"` to simulate a failed inflow.
The deposit lands as a transaction and is reported by webhook. See the [Sandbox testing guide](/guides/testing-in-sandbox) for how virtual accounts fit into the broader sandbox model.
# Get Exchange Rates
Source: https://docs.afriex.com/api-reference/endpoint/rates/get
GET /api/v1/org/rates
Retrieves real-time exchange rates.
Retrieves real-time exchange rates. Use the optional `fromSymbols` and `toSymbols` query parameters to filter results to specific currency pairs.
# Register SME
Source: https://docs.afriex.com/api-reference/endpoint/sme-registration/register
POST /api/v1/sme-registration
Registers a business as its own SME so that dedicated virtual accounts are issued under the business's own legal entity rather than the shared default. The flow is stateful and driven by this single endpoint: the `step` field selects the payload carried in `data`. Call the steps in order — `INITIATE` (triggers a one-time passcode to the supplied mobile number), `CONFIRM_OTP`, then `SUBMIT` (company details, directors and documents). Upload each document first via `POST /api/v1/media/url` and pass the returned object keys in the `SUBMIT` payload. Requires the `COMPLIANCE.KYB.SUBMIT` key permission.
**Only relevant if you want dedicated KES virtual accounts.** This flow is currently used only to register your business so that **KES virtual accounts** can be issued under your own legal entity in Kenya. It is **not required for any other corridor** (NGN, USD, ZAR, GHS, etc.); skip this page if you are not planning to collect KES via dedicated virtual accounts.
Register your business as its own SME in Kenya so that dedicated **KES virtual accounts** are issued under your legal entity instead of the shared Afriex default. This is a **stateful, step-based** flow driven by a single endpoint: the `step` field selects which payload shape goes in `data`.
Requires an API key with **admin** permission. Only Limited Liability Companies are supported today, so `businessType` must be `2` in the `INITIATE` step.
## Flow
Send the business `mobile`, `email`, and `businessType`. Afriex creates an onboarding request, returns an `onboardingRequestId`, and sends a one-time passcode to the supplied mobile number.
Post back the `onboardingRequestId` from step 1 and the 6-digit `otp` the customer received.
Submit company details, `directors[]`, and optional `organizationShareholders[]` / `media[]`. Every file field is an **object key** returned by [Generate Upload URL](/api-reference/endpoint/media/generate-upload-url). Upload each file first, then reference the returned `key` here.
Track progress in `status`: `OTP_PENDING` after `INITIATE`, `DETAILS_PENDING` after `CONFIRM_OTP`, and `SUBMITTED` after `SUBMIT`. The `nextStep` field tells you what to call next; `expiresAt` is the deadline for that step (OTP window after `INITIATE`, submit window after `CONFIRM_OTP`).
## Mobile number format
Every mobile number, the applicant's on `INITIATE` and each director's on `SUBMIT`, must be a valid **Kenyan** number. Any of these forms is accepted and normalised to E.164:
| Format | Example |
| - | - |
| E.164 with `+` | `+254712345678` |
| International without `+` | `254712345678` |
| National with leading zero | `0712345678` |
| National without leading zero | `712345678` |
A non-Kenyan number is rejected with `422`. Directors do not need a `countryCode`, it is derived from the number, and is ignored if you send one.
Once submitted, poll [Get SME Registration Status](/api-reference/endpoint/sme-registration/status) for the review outcome. Dedicated KES virtual accounts under your entity are issued only after the review reaches `APPROVED`.
# Get SME Registration Status
Source: https://docs.afriex.com/api-reference/endpoint/sme-registration/status
GET /api/v1/sme-registration/status
Returns the most recent SME registration status for the authenticated business. Once the registration has been submitted, the response also includes the latest review outcome. Requires the `COMPLIANCE.KYB.READ` key permission.
Returns the most recent [SME registration](/api-reference/endpoint/sme-registration/register) state for the authenticated business. Once submitted, the response also includes the latest review outcome so you can gate downstream flows (for example, issuing dedicated KES virtual accounts) on approval.
Only relevant if you are pursuing dedicated **KES virtual accounts** via [Register SME](/api-reference/endpoint/sme-registration/register). Other corridors do not use this flow. Requires an API key with **admin** permission. If the business has never registered, `onboardingRequestId` and `status` come back `null`.
## Review outcomes
`reviewStatus` is present only after `SUBMIT`. Dedicated KES virtual accounts under your entity are issued only after `APPROVED`.
| Value | Meaning |
| - | - |
| `SUBMITTED` | Registration has been received and is waiting to be picked up for review. |
| `PROCESSING` | The review is in progress. |
| `APPROVED` | Approved. Dedicated KES virtual accounts will now be issued under your entity. |
| `REJECTED` | Rejected. See `rejectReasons[]` for the human-readable reasons. |
| `CLOSED` | The reviewing party marked the account as closed. Contact support. |
| `UNDER_REVIEW` | Held for a manual review; no action from you until the outcome flips. |
| `UNKNOWN` | Forward-compatibility slot for any outcome Afriex has not mapped yet. Treat as "no decision yet" and poll again later. |
When the review is `REJECTED`, `rejectReasons[]` carries human-readable reasons you can surface to the operator.
The set of `reviewStatus` values may grow over time as new outcomes are surfaced. Existing values will not change meaning; handle unfamiliar values by falling back to `UNKNOWN`.
## Freshness
The response may also include `isReviewStatusStale: true`. That flag means Afriex could not reach the review provider on this read, so the `reviewStatus` reflects the last known value (or `UNKNOWN` if none is cached) rather than the current one. Poll again later for a fresh read; the field is absent when the read succeeded.
# Get Settlement Advice
Source: https://docs.afriex.com/api-reference/endpoint/transactions/advice
GET /api/v1/transaction/{transactionId}/advice
Returns a short-lived presigned download URL for the settlement & remittance advice PDF auto-generated for a USD withdrawal from your collection wallet (a withdrawal created with `settlement: "request"`). The advice is issued with status `PENDING` when the withdrawal is created and regenerated with status `COMPLETED` (same reference, incremented version) once the withdrawal succeeds. Scoped to your business: you can only fetch advices for your own transactions. This document is a settlement and remittance advice, not a tax invoice.
Returns a short-lived (5 minute) presigned download URL for the settlement & remittance advice PDF that Afriex auto-generates for a USD withdrawal from your **Collection wallet**, i.e. a `WITHDRAW` created with `meta.settlement: "request"`.
Advices are only generated for USD withdrawals with `meta.settlement: "request"`. Requesting an advice for any other transaction returns `404`.
## Lifecycle
The advice is issued twice for the same transaction:
When the withdrawal is created, an advice is generated with `status: "PENDING"` and `version: 1`.
Once the withdrawal succeeds, the advice is regenerated as `status: "COMPLETED"` with an incremented `version`. The `reference` stays the same across regenerations.
## The document
This is a **settlement and remittance advice**, not a tax invoice. Use it to reconcile the settlement leg of a collection-wallet USD withdrawal.
## Scope and access
Advices are scoped to your business: you can only fetch advices for transactions you created. The download `url` is a presigned S3 URL valid for 5 minutes. Fetch a fresh URL by calling this endpoint again when the current one expires.
# Authorize Transaction
Source: https://docs.afriex.com/api-reference/endpoint/transactions/authorize
POST /api/v1/transaction/{transactionId}/authorize
Completes a transaction that was created in a `CUSTOMER_ACTION_REQUIRED` state and needs an extra authorization step (for example, a one-time password on a mobile-money deposit). The body is discriminated by `type`; today the only supported variant is `OTP`.
Complete a transaction that was created in a `CUSTOMER_ACTION_REQUIRED` state and needs an extra authorization step, such as a one-time password (OTP) on a mobile-money deposit. The request body is discriminated by `type`; today the only supported variant is `OTP`.
A deposit needs this step when the create-transaction response comes back with status `CUSTOMER_ACTION_REQUIRED` and `meta.otpRequired` set to `true`.
**Testing in sandbox?** Submit the OTP `123456` to complete the deposit. Any other value is rejected so you can test the wrong-OTP path. See [Simulating OTP-required deposits](/development#simulating-otp-required-deposits) for the full sandbox flow.
# Create Transaction
Source: https://docs.afriex.com/api-reference/endpoint/transactions/create
POST /api/v1/transaction
Create a new transaction to process a payment for a customer. Use `type: WITHDRAW` to send funds to a destination payment method (requires `destinationId`), `type: DEPOSIT` to pull funds from a source payment method (requires `sourceId`), or `type: SWAP` to convert funds between currencies within the Afriex wallet (requires `sourceCurrency`, `destinationCurrency`, `meta`, and exactly one of `sourceAmount` or `destinationAmount`).
Create a new transaction to process a payment for a customer. Use `type: WITHDRAW` to send funds to a destination payment method (requires `destinationId`), `type: DEPOSIT` to pull funds from a source payment method (requires `sourceId`), or `type: SWAP` to convert funds between currencies within the Afriex wallet. A SWAP requires `sourceCurrency`, `destinationCurrency`, `meta`, and **exactly one** of `sourceAmount` or `destinationAmount`: the API computes the other side at the live exchange rate. Providing both amounts is rejected.
Want to show a customer what a withdrawal will cost before you create it? [Quote a Withdrawal](/api-reference/endpoint/transactions/quote) returns the rate, fee, and both amounts without creating anything, and a create with the same inputs charges the quoted amounts.
**Testing in sandbox?** Sandbox transactions settle automatically in about 5 minutes and send the `TRANSACTION.UPDATED` webhook. Pick the outcome with the payment method's last four digits (e.g. `0001` fails, `0002` requires OTP for mobile money, `0003` stays pending until you call [`/simulate`](/api-reference/endpoint/transactions/simulate)). Add `SIMULATE_INSTANT` anywhere in `meta.reference` to settle in about 30 seconds. Full reference: [Testing in Sandbox](/guides/testing-in-sandbox).
# Get Transaction
Source: https://docs.afriex.com/api-reference/endpoint/transactions/get
GET /api/v1/transaction/{transactionId}
Retrieves a specific transaction by its unique identifier.
Retrieve a specific transaction by its unique identifier.
# List Transactions
Source: https://docs.afriex.com/api-reference/endpoint/transactions/list
GET /api/v1/transaction
Retrieves a paginated list of transactions.
Retrieve a paginated list of all transactions associated with your business.
# Submit Pool-Account Payment Proof
Source: https://docs.afriex.com/api-reference/endpoint/transactions/pool-account-proof
POST /api/v1/transaction/pool-account
Submits proof of a deposit made to the business pool account. The request creates a business deposit transaction in `IN_REVIEW` status; an operator confirms the bank inflow before the funds are credited. The response returns that transaction. Duplicate submissions of the same business, customer, amount, reference, timestamp and sender details within the configured idempotency window are rejected.
Submit proof of a deposit made into your business's [pool account](/api-reference/endpoint/payment-methods/pool-account). The request creates a business deposit transaction in `IN_REVIEW`; an operator confirms the bank inflow before the funds are credited.
## Flow
Call [Generate Upload URL](/api-reference/endpoint/media/generate-upload-url) with `type: "transaction"` and `PUT` the proof file (bank receipt, screenshot, etc.) to the returned presigned URL.
The `type` matters. A URL requested with `type: "user"` uploads to a different bucket; passing that `key` here is rejected as not found.
Call this endpoint with the deposit `amount`, the `countryCode` of the pool account, the pool-account `reference` (see below), the S3 `fileKey` returned in step 1, and the payment `timestamp`. Optionally attach `senderDetails` (name, account number, bank, country) to help reconciliation.
The response is a transaction in `IN_REVIEW`. Subscribe to [Transaction Events](/api-reference/endpoint/webhooks/transaction-events) to be notified when the operator confirms and the status transitions.
## Choosing the `reference`
* Credit an **end customer**: pass the customer's `reference` (the shortened value returned on customer reads; falls back to the customer id when none is assigned). `customerId` must match that customer.
* Credit the **business itself**: pass the business's own pool-account reference from [Get Pool Account](/api-reference/endpoint/payment-methods/pool-account). Routing does not use `customerId` in this case.
## Idempotency
Duplicate submissions with the same business, `customerId`, `amount`, `reference`, `timestamp`, and `senderDetails` within the configured window are rejected with `409 DUPLICATE_REQUEST`. Include a distinct `timestamp` per real payment.
# Quote a Withdrawal
Source: https://docs.afriex.com/api-reference/endpoint/transactions/quote
GET /api/v1/transaction/quote
Returns what a withdrawal would cost, priced by the same code that charges it, without creating anything. Type exactly one of `sourceAmount` (what you send) or `destinationAmount` (what the recipient receives); the other is derived from it. The side you type is returned as `anchor`.
`rate`, `fee` and the amounts mean exactly what they mean on the transaction a create returns, so a create with the same inputs charges the quoted amounts. A quote does not check limits or customer verification and does not need an idempotency key or reference; those apply when you create the transaction, so a quote can succeed where the create is refused. Quotes are not reserved: rates and promotions can change before you create.
Name the destination either with `destinationId` (a saved payment method) or with `destinationChannel` and `destinationCountry` (when the customer has no account to send to yet), not both. `destinationCountry` can be left out for `SWIFT` and `CRYPTO`. A quote priced from a channel alone comes back with `indicative: true`: the account is not known, so the processor that would carry the payout, and any fee that depends on it, may differ when you create. `customerId` is optional; without it the quote is priced for the business owner, so a fee or promotion that depends on the customer's country can differ when you create.
Price a withdrawal before you create it. A quote returns the rate, the fee, and both amounts, and it creates nothing, so you can show your customer exactly what a send will cost.
A quote is priced by the same logic that charges a transaction. If you [create the transaction](/api-reference/endpoint/transactions/create) with the same inputs, you are charged the quoted amounts.
Requires the `TRANSACTION.WITHDRAW.CREATE` key permission. See [Permissions](/api-reference/permissions).
## How to use it
Send exactly one of `sourceAmount` (what you send) or `destinationAmount` (what the recipient receives). The API derives the other side. The response `anchor` tells you which side is exact: `source` or `destination`.
Send either `destinationId` (a saved payment method) or `destinationChannel` plus `destinationCountry` (when the customer has no account to send to yet), never both. `destinationCountry` can be left out for `SWIFT` and `CRYPTO`.
`customerId` is optional. Without it, the quote is priced for the business owner.
## Example
```bash theme={null}
curl -G "https://sandbox.api.afriex.com/api/v1/transaction/quote" \
-H "x-api-key: YOUR_API_KEY" \
--data-urlencode "sourceCurrency=NGN" \
--data-urlencode "destinationCurrency=USD" \
--data-urlencode "sourceAmount=100000" \
--data-urlencode "destinationId=PAYMENT_METHOD_ID"
```
```json theme={null}
{
"data": {
"anchor": "source",
"sourceAmount": "100000",
"sourceCurrency": "NGN",
"destinationAmount": "65.76696108",
"destinationCurrency": "USD",
"rate": "0.0006576696108"
}
}
```
`rate` reads as `1 sourceCurrency = rate destinationCurrency`. `fee` is in `sourceCurrency` and is left out when no fee applies.
## Things to know
* **Quotes are not reserved.** Rates and promotions can change before you create, so quote again close to the send if the price matters.
* **A quote can succeed where the create is refused.** A quote does not check limits or customer verification, and it needs no idempotency key or reference. Those apply when you create the transaction.
* **`indicative: true`.** When you price from `destinationChannel` alone, the account is not known yet, so the payout route and any fee that depends on it can differ when you create. The response carries `indicative: true` to tell you so.
* **Pass `customerId` for an exact price.** A fee or promotion that depends on the customer's country can differ from a business-owner quote.
## Errors
A `422` means the request is invalid: you sent both amounts or neither, both `destinationId` and `destinationChannel` or neither, or a required parameter is missing.
# Simulate Transaction Outcome
Source: https://docs.afriex.com/api-reference/endpoint/transactions/simulate
POST /api/v1/transaction/{transactionId}/simulate
Sandbox only; returns 403 in production. Completes a pending sandbox deposit or payout with the outcome you choose, and your balance and transaction webhook behave as they would in production. The transaction must still be `PENDING`, `PROCESSING` or `UNKNOWN`: a deposit waiting for an OTP must be authorized first. Use it for a payment method whose account number, phone number or card ends in `0003`, which stays pending until simulated, or to complete any other pending sandbox transaction early. The result arrives by webhook shortly after this call returns.
**Sandbox only.** Returns `403` in production.
Completes a pending sandbox transaction with the outcome you choose. Your balance and the `TRANSACTION.UPDATED` webhook behave exactly as they would in production.
Use this whenever you want a deterministic outcome without waiting on the default sandbox settlement window (\~5 minutes):
* **Pending on purpose.** A payment method whose account number, phone number or card ends in `0003` stays `PENDING` until you call this endpoint.
* **Speed up any other pending sandbox transaction.** Call it to finalize sooner than the \~5 minute default.
The transaction must still be `PENDING`, `PROCESSING` or `UNKNOWN`. A deposit waiting on OTP (`CUSTOMER_ACTION_REQUIRED`) must be authorized via [Authorize Transaction](/api-reference/endpoint/transactions/authorize) first.
See the [Sandbox testing guide](/guides/testing-in-sandbox) for the full test-value taxonomy.
# Checkout Session Events
Source: https://docs.afriex.com/api-reference/endpoint/webhooks/checkout-session-events
POST checkout_session.events
Afriex delivers a real-time notification to your configured webhook URL when a hosted **checkout session** is created.
## Security
See [Webhooks Introduction](/api-reference/endpoint/webhooks/introduction) for setup, signature validation, and retry behavior.
## Event Types
* `CHECKOUT_SESSION.CREATED` - A new hosted checkout session has been created
## Sample Payload
### Checkout Session Created
```json theme={null}
{
"event": "CHECKOUT_SESSION.CREATED",
"data": {
"sessionId": "69528240ba52c13b669fb239",
"merchantReference": "order-12345",
"amount": 5000,
"currency": "NGN",
"expiresAt": "2025-12-29T13:45:10.123Z",
"createdAt": "2025-12-29T13:30:22.554Z",
"metadata": {},
"customer": {
"name": "John Doe",
"email": "john.doe@example.com",
"phone": "+2348192837465",
"countryCode": "NG"
}
}
}
```
Once the session is paid, the payload also includes `afriexTransactionId` and `paidAt`. Subscribe to [Transaction Events](/api-reference/endpoint/webhooks/transaction-events) to track the resulting transaction through to settlement.
## Response
Return `200` (or another `2xx`) after successful validation and processing.
# Customer Events
Source: https://docs.afriex.com/api-reference/endpoint/webhooks/customer-events
POST customer.events
Afriex delivers real-time notifications to your configured webhook URL whenever a customer is **created**, **updated**, or **deleted**.
## Security
See [Webhooks Introduction](/api-reference/endpoint/webhooks/introduction) for setup, signature validation, and retry behavior.
## Event Types
* `CUSTOMER.CREATED` - A new customer has been created
* `CUSTOMER.UPDATED` - Customer information has been updated
* `CUSTOMER.DELETED` - A customer has been deleted
## Sample Payloads
### Customer Created
```json theme={null}
{
"event": "CUSTOMER.CREATED",
"data": {
"name": "John Doe",
"email": "johndoe@gmail.com",
"phone": "+2348012345678",
"customerId": "698b0440cba7ec3daee9163d",
"countryCode": "NG",
"meta": {
"source": "whatsapp"
},
"createdAt": "2026-02-10T10:11:12.415Z",
"updatedAt": "2026-02-10T10:11:12.415Z"
}
}
```
### Customer Updated
```json theme={null}
{
"event": "CUSTOMER.UPDATED",
"data": {
"name": "John Smith",
"email": "johnsmith@gmail.com",
"phone": "+2348012345678",
"customerId": "698b0440cba7ec3daee9163d",
"countryCode": "NG",
"meta": {
"source": "whatsapp",
"verified": true
},
"createdAt": "2026-02-10T10:11:12.415Z",
"updatedAt": "2026-02-11T15:30:45.123Z"
}
}
```
### Customer Deleted
```json theme={null}
{
"event": "CUSTOMER.DELETED",
"data": {
"name": "John Smith",
"email": "johnsmith@gmail.com",
"phone": "+2348012345678",
"customerId": "698b0440cba7ec3daee9163d",
"countryCode": "NG",
"meta": {
"source": "whatsapp",
"verified": true
},
"createdAt": "2026-02-10T10:11:12.415Z",
"updatedAt": "2026-02-11T15:30:45.123Z"
}
}
```
The `event` field will be `CUSTOMER.CREATED`, `CUSTOMER.UPDATED`, or `CUSTOMER.DELETED` depending on the action that triggered the notification.
Delete events carry the **same full customer object** as create and update events. Afriex reads the record before removing it and sends it in full, so `CUSTOMER.DELETED` gives you the customer's last known state. There is no slimmed-down delete payload and no `deletedAt` field.
## Response
Return `200` (or another `2xx`) after successful validation and processing.
# Introduction
Source: https://docs.afriex.com/api-reference/endpoint/webhooks/introduction
Webhook overview, security, and verification
Afriex sends real-time webhook notifications to your configured endpoint when important resource changes happen.
## How webhooks work
Afriex delivers event notifications for customer, payment method, transaction, and checkout session updates to the webhook URL you configure in your dashboard.
## Set up your webhook endpoint
1. Open the **Dashboard**.
2. Go to **Developers**.
3. Select the **Webhooks** tab.
4. Save your webhook URL.
Example webhook URLs:
* `https://your-domain.com/webhook`
After you save your webhook URL, you can copy your webhook public key from the same screen.
## Afriex IP addresses
**Allowlist these IPs before going live.** To receive webhook deliveries, your server must allow inbound traffic from the following Afriex IP addresses:
| Environment | IP Address |
| - | - |
| Sandbox | `34.234.189.210` |
| Production | `34.197.33.100` |
Add these to your firewall allowlist. Without this, Afriex webhook requests will be blocked.
## Security and signature validation
Every webhook request includes an `x-webhook-signature` header. You must validate this signature before processing the payload.
* Header: `x-webhook-signature`
* Algorithm: ECDSA with SHA-256
* Key: elliptic-curve, `secp384r1` (P-384). It is **not** an RSA key.
* Signature format: DER-encoded, then base64-encoded
* Signed data: the exact raw request body (`string`/`Buffer`)
* Public key source: **Dashboard -> Developers -> Webhooks**
* Environment note: Staging and production use different public keys
Always verify against the raw request body bytes. Do not verify parsed JSON, and do not re-serialize the body before you verify it.
The key is elliptic-curve, not RSA. A verifier built for RSA (for example `RSA-SHA256` with PKCS#1 v1.5) rejects every delivery. Use ECDSA with the P-384 curve and SHA-256.
Libraries that expect a raw `r||s` (IEEE P1363) signature, such as WebCrypto and JOSE libraries, need the DER signature converted first. Node's `crypto` module reads DER by default, so the sample below needs no conversion.
```typescript theme={null}
import crypto from "crypto";
function verifySignature(
signature: string,
rawBody: string | Buffer,
publicKey: string
): boolean {
try {
// The key is an EC (P-384) key, so this verifies ECDSA with SHA-256.
const verifier = crypto.createVerify("SHA256");
verifier.update(rawBody);
return verifier.verify(publicKey, signature, "base64");
} catch {
return false;
}
}
```
## Response and retries
Return a success response quickly after verification and processing.
* On success: return `200` (or another `2xx` status). Afriex waits up to **30 seconds** for your response before treating the attempt as failed, so aim well inside that budget and do slow work after acknowledging.
* On verification failure: return `400` or `401`
* Retry behavior: Afriex retries failed deliveries up to 12 times with exponential backoff (starting at 30 seconds).
The intended retry schedule is:
* 30s → 1m → 2m → 4m → 8m → 16m → 32m → 1h → 2h → 4h → 8h → 16h
## Webhook event references
* [Customer Events](/api-reference/endpoint/webhooks/customer-events)
* [Payment Method Events](/api-reference/endpoint/webhooks/payment-method-events)
* [Transaction Events](/api-reference/endpoint/webhooks/transaction-events)
* [Checkout Session Events](/api-reference/endpoint/webhooks/checkout-session-events)
* [Pool Deposit Events](/api-reference/endpoint/webhooks/pool-deposit-events)
# Payment Method Events
Source: https://docs.afriex.com/api-reference/endpoint/webhooks/payment-method-events
POST payment_method.events
Afriex delivers real-time notifications to your configured webhook URL whenever a payment method is **created**, **updated**, or **deleted**.
## Security
See [Webhooks Introduction](/api-reference/endpoint/webhooks/introduction) for setup, signature validation, and retry behavior.
## Event Types
* `PAYMENT_METHOD.CREATED` - A new payment method has been created
* `PAYMENT_METHOD.UPDATED` - Payment method information has been updated
* `PAYMENT_METHOD.DELETED` - A payment method has been deleted
## Sample Payloads
### Payment Method Created
```json theme={null}
{
"event": "PAYMENT_METHOD.CREATED",
"data": {
"paymentMethodId": "6936b8774f987b2994865b73",
"channel": "BANK_ACCOUNT",
"customerId": "6922e4520a53e858ab42efa8",
"institution": {
"institutionCode": "058",
"institutionName": "GTBank"
},
"accountName": "John Doe",
"accountNumber": "1234567890",
"countryCode": "NG"
}
}
```
### Payment Method Updated
```json theme={null}
{
"event": "PAYMENT_METHOD.UPDATED",
"data": {
"paymentMethodId": "6936b8774f987b2994865b73",
"channel": "BANK_ACCOUNT",
"customerId": "6922e4520a53e858ab42efa8",
"institution": {
"institutionCode": "058",
"institutionName": "GTBank"
},
"accountName": "John Smith",
"accountNumber": "1234567890",
"countryCode": "NG"
}
}
```
### Payment Method Deleted
```json theme={null}
{
"event": "PAYMENT_METHOD.DELETED",
"data": {
"paymentMethodId": "69f87b0dcc0ee96511560796",
"channel": "BANK_ACCOUNT",
"customerId": "6922e4520a53e858ab42efa8",
"institution": {
"institutionCode": "058",
"institutionName": "GTBank"
},
"accountName": "John Smith",
"accountNumber": "1234567890",
"countryCode": "NG"
}
}
```
The `event` field will be `PAYMENT_METHOD.CREATED`, `PAYMENT_METHOD.UPDATED`, or `PAYMENT_METHOD.DELETED` depending on the action that triggered the notification.
## Response
Return `200` (or another `2xx`) after successful validation and processing.
# Pool Deposit Events
Source: https://docs.afriex.com/api-reference/endpoint/webhooks/pool-deposit-events
POST pool_deposit_request.events
Afriex delivers a real-time notification to your configured webhook URL when a **pool account deposit** you submitted is rejected during review.
Submitting proof of a pool-account payment through [Submit Pool Account Payment Proof](/api-reference/endpoint/transactions/pool-account-proof) creates a transaction in `IN_REVIEW`. An operator then confirms or rejects the bank inflow. Approval surfaces as a `TRANSACTION.CREATED` event; rejection surfaces here.
## Security
See [Webhooks Introduction](/api-reference/endpoint/webhooks/introduction) for setup, signature validation, and retry behavior.
## Event Types
* `POOL_DEPOSIT_REQUEST.REJECTED` - A submitted pool account deposit was rejected during review
## Sample Payload
### Pool Deposit Rejected
```json theme={null}
{
"event": "POOL_DEPOSIT_REQUEST.REJECTED",
"data": {
"transactionId": "69528240ba52c13b669fb239",
"reference": "pool-account-reference",
"amount": 5000,
"currency": "NGN",
"rejectionReason": "Sender name does not match the submitted proof",
"resubmissionRequired": true
}
}
```
`rejectionReason` is free text entered by the reviewer. Show it to your operator; do not branch on its contents. Branch on `resubmissionRequired` instead: `true` means you can correct the submission and send fresh proof, `false` means the rejection is final.
This event is delivered by the pool-account review flow only. It cannot be fired from the sandbox [Trigger Test Webhook](/api-reference/endpoint/webhooks/trigger) endpoint, so test it by submitting proof in sandbox and having it rejected.
This payload is a flat, purpose-built shape rather than the full transaction object used by [Transaction Events](/api-reference/endpoint/webhooks/transaction-events). If you need the full transaction, fetch it with [Get Transaction](/api-reference/endpoint/transactions/get) using `transactionId`.
## Response
Return `200` (or another `2xx`) after successful validation and processing.
# Transaction Events
Source: https://docs.afriex.com/api-reference/endpoint/webhooks/transaction-events
POST transaction.events
Afriex sends real-time webhook notifications to your configured URL whenever a transaction is **created** or **updated**.
## Security
See [Webhooks Introduction](/api-reference/endpoint/webhooks/introduction) for setup, signature validation, and retry behavior.
## Event Types
* `TRANSACTION.CREATED` - A new transaction has been initiated
* `TRANSACTION.UPDATED` - Transaction status or fields have changed
## Sample Payloads
### Transaction Created
```json theme={null}
{
"event": "TRANSACTION.CREATED",
"data": {
"status": "PENDING",
"type": "WITHDRAW",
"channel": "BANK_ACCOUNT",
"sourceAmount": "5000",
"sourceCurrency": "NGN",
"destinationAmount": "5000",
"destinationCurrency": "NGN",
"destinationId": "68e676bac48ad6beae3559a0",
"customerId": "68e6717848e1f632e9686460",
"transactionId": "69eb343d4973a18671813fb3",
"merchantReference": "myReference",
"meta": {
"reference": "myReference",
"idempotencyKey": "myReference",
"narration": "My first transaction"
},
"createdAt": "2026-04-24T09:13:33.883Z",
"updatedAt": "2026-04-24T09:13:33.883Z"
}
}
```
### Transaction Updated
```json theme={null}
{
"event": "TRANSACTION.UPDATED",
"data": {
"status": "PENDING",
"type": "WITHDRAW",
"channel": "BANK_ACCOUNT",
"sourceAmount": "5000",
"sourceCurrency": "NGN",
"destinationAmount": "5000",
"destinationCurrency": "NGN",
"destinationId": "68e676bac48ad6beae3559a0",
"customerId": "68e6717848e1f632e9686460",
"transactionId": "69eb343d4973a18671813fb3",
"merchantReference": "myReference",
"meta": {
"reference": "myReference",
"idempotencyKey": "myReference",
"narration": "My first transaction"
},
"createdAt": "2026-04-24T09:13:33.883Z",
"updatedAt": "2026-04-24T09:15:45.123Z"
}
}
```
### Transaction Failed
When `status` transitions to `FAILED` or `REJECTED`, the payload's `meta.failureReason` describes why using a stable `AFX_*` code and a customer-safe message. See [Failure reasons](/api-reference/models#failure-reasons) for the full code list.
```json theme={null}
{
"event": "TRANSACTION.UPDATED",
"data": {
"status": "FAILED",
"type": "WITHDRAW",
"channel": "BANK_ACCOUNT",
"sourceAmount": "5000",
"sourceCurrency": "NGN",
"destinationAmount": "5000",
"destinationCurrency": "NGN",
"destinationId": "68e676bac48ad6beae3559a0",
"customerId": "68e6717848e1f632e9686460",
"transactionId": "69eb343d4973a18671813fb3",
"merchantReference": "myReference",
"meta": {
"reference": "myReference",
"idempotencyKey": "myReference",
"narration": "My first transaction",
"failureReason": {
"code": "AFX_VELOCITY_LIMIT_EXCEEDED",
"message": "This transaction exceeds the allowed velocity limit. Try again later.",
"retryable": true
}
},
"createdAt": "2026-04-24T09:13:33.883Z",
"updatedAt": "2026-04-24T09:17:02.410Z"
}
}
```
The `event` field will be `TRANSACTION.CREATED` or `TRANSACTION.UPDATED` depending on the action that triggered the notification.
Cross-currency transactions also carry a `rate` field alongside the amounts, holding the realized source-to-destination exchange rate. See [Transaction](/api-reference/models#transaction) for the full field list.
`TRANSACTION.UPDATED` is **not** a terminal-status-only signal. It fires on every status change Afriex considers worth reporting, including intermediate ones such as `PROCESSING` and `CUSTOMER_ACTION_REQUIRED`, so a single transaction can produce several. Always branch on the payload's `status` field rather than treating any delivery as final.
## Response
Return `200` (or another `2xx`) after successful validation and processing.
# Trigger Test Webhook
Source: https://docs.afriex.com/api-reference/endpoint/webhooks/trigger
POST /api/v1/webhooks/trigger
Fires a real signed webhook to the business's configured callback URL using a real entity (customer, payment method, transaction, or checkout session) as the payload. Useful for testing webhook handlers in sandbox without manufacturing the underlying activity. **This endpoint is only available in the sandbox/staging environment and will return a `403 Forbidden` response in production.**
Fire a real signed webhook from sandbox using an existing entity as the payload. Use this to test your webhook handler without having to manufacture the underlying activity.
This endpoint is only available in the **sandbox/staging** environment and returns a `403 Forbidden` response in production.
To test transaction webhooks end to end, let a real sandbox transaction settle instead of firing a synthetic event: it reaches a final status in about 5 minutes and sends `TRANSACTION.UPDATED`. Pick the outcome with the payment method's last four digits (`0001` fails, `0003` stays pending until you call [`/simulate`](/api-reference/endpoint/transactions/simulate)), or add `SIMULATE_INSTANT` anywhere in `meta.reference` to speed things up to about 30 seconds. Full reference: [Testing in Sandbox](/guides/testing-in-sandbox).
## How it works
1. Create or look up an existing entity in sandbox.
2. Call this endpoint with the matching `event` name and the entity's ID.
3. Afriex delivers a real, signed webhook to your configured callback URL using that entity as the payload.
Because the payload is a real entity, your handler verifies the signature and processes the event exactly the same way it would in production.
## What to pass as `entityId`
| Event family | `entityId` |
| - | - |
| `CUSTOMER.*` | The customer ID |
| `PAYMENT_METHOD.*` | The payment method ID |
| `TRANSACTION.*` | The transaction ID |
| `CHECKOUT_SESSION.*` | The checkout session ID |
# Introduction
Source: https://docs.afriex.com/api-reference/introduction
Welcome to the Afriex Business API
## Welcome to the Afriex Business API
The Afriex Business API enables you to manage customers, process payments, handle payouts, and receive real-time notifications via webhooks. This documentation provides comprehensive guides and API reference for integrating with our platform.
Get the full OpenAPI spec at this endpoint for code generation, SDKs, and API tooling.
Add the Afriex Business API to your MCP client and search the docs from Cursor, Claude Code, and other MCP-enabled tools.
## Base URLs
The API is available on two environments:
| Environment | Base URL |
| - | - |
| Staging | `https://sandbox.api.afriex.com` |
| Production | `https://api.afriex.com` |
## Authentication
All API endpoints require authentication using an API key. Include your API key in the `x-api-key` header:
```
x-api-key: your-api-key-here
```
Obtain your API key from the [Afriex Dashboard](https://business.afriex.com/).
### API key permissions
A business can provision **multiple API keys**, each scoped to a configurable set of **permissions** (for example, read transactions or create deposits). Permissions are chosen per key at creation time and can be revoked by deleting the key. Manage your keys and their permissions under **Developer → API keys** in the dashboard.
* A key that is missing the permission required by an endpoint is rejected with `401 Unauthorized`.
* An unrecognised, malformed, or revoked key returns `401 Unauthorized` as well.
These two cases are deliberately indistinguishable on the wire. If a request that used to work starts returning `401`, check the key's permissions in the dashboard before assuming the key itself is bad.
### Request signing
Some businesses have payload signing enabled. When it is, send the request signature in the optional `x-api-signature` header:
```
x-api-signature: your-request-signature
```
If signing is not enabled for your business, this header is not required. Contact [support@afriex.com](mailto:support@afriex.com) to enable it.
## API Versioning
The API supports versioning via the `x-api-version` header:
```
x-api-version: 2026-05-18
```
`2026-05-18` is currently the only supported version, and it is what you get if you omit the header. Any other value is rejected with `400 Bad Request` and an `Unsupported API version` message.
## Core Resources
The API provides the following core resources:
Create and manage customer profiles with optional KYC information.
Set up bank accounts, mobile money, and crypto wallets for payouts.
Process deposits and withdraws with real-time status tracking.
Receive real-time notifications for customer, payment method, and transaction events.
**Webhook firewall requirement:** If you plan to receive webhooks, allowlist the following Afriex IP addresses on your server:
* Sandbox: `34.234.189.210`
* Production: `34.197.33.100`
See [Webhook IP addresses](/api-reference/endpoint/webhooks/introduction#afriex-ip-addresses) for details.
## Quick Links
* [Quickstart Guide](/quickstart) - Get up and running in minutes
* [Customer API](/api-reference/endpoint/customers/create) - Manage customer profiles
* [Transaction API](/api-reference/endpoint/transactions/create) - Process payments
* [Webhook Events](/api-reference/endpoint/webhooks/introduction) - Handle notifications
## Support
Need help? Contact us at [support@afriex.com](mailto:support@afriex.com).
# Models
Source: https://docs.afriex.com/api-reference/models
Data models, enumerations, and field references for the Afriex Business API
This page documents the key data models returned and accepted by the API, including all enumerated values and field-level details.
***
## Customer
| Field | Type | Description |
| - | - | - |
| `customerId` | string | Unique identifier for the customer |
| `reference` | string | Shortened customer reference. Returned on customer reads (list and by-ID) and used as the `reference` when submitting a [pool-account payment proof](/api-reference/endpoint/transactions/pool-account-proof). Falls back to `customerId` when no shortened value has been assigned. |
| `name` | string | Full name of the customer |
| `email` | string (email) | Email address |
| `phone` | string | Phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) (e.g. `+2348192837465`) |
| `countryCode` | string | ISO 3166-1 alpha-2 country code (e.g. `NG`) |
| `meta` | object | Optional metadata you attach to the customer |
| `createdAt` | string (ISO 8601) | Timestamp when the customer was created |
| `updatedAt` | string (ISO 8601) | Timestamp of the last update |
On create and update requests, supply the customer's full name as `fullName`; responses return it as `name`. KYC details are submitted separately through the [Update KYC](/api-reference/endpoint/customers/update-kyc) endpoint.
***
## Transaction
| Field | Type | Description |
| - | - | - |
| `transactionId` | string | Unique identifier for the transaction |
| `customerId` | string | ID of the customer who owns this transaction |
| `sourceId` | string | ID of the source payment method |
| `destinationId` | string | ID of the destination payment method |
| `sourceAmount` | string | Amount debited from the source wallet |
| `sourceCurrency` | string | ISO 4217 currency code of the source wallet (e.g. `USD`) |
| `destinationAmount` | string | Amount credited to the destination |
| `destinationCurrency` | string | ISO 4217 currency code of the destination (e.g. `NGN`) |
| `rate` | string | Realized source-to-destination exchange rate, as `1 sourceCurrency = rate destinationCurrency` (equal to `destinationAmount / sourceAmount`) |
| `fee` | string | Fee charged for this transaction, denominated in `sourceCurrency`. Reported separately from `sourceAmount`. Omitted when no fee applied. |
| `type` | string | Transaction type. See [Transaction Types](#transaction-types) |
| `channel` | string | Payment channel of the transaction. See [Transaction Channels](#transaction-channels). |
| `status` | string | Current status. See [Transaction Statuses](#transaction-statuses) |
| `merchantReference` | string | Merchant-supplied reference (mirrors `meta.reference` from the create request) |
| `meta` | object | Metadata attached to the transaction. See [TransactionMeta](#transactionmeta) |
| `createdAt` | string (ISO 8601) | Timestamp when the transaction was created |
| `updatedAt` | string (ISO 8601) | Timestamp of the last update |
### Transaction Types
| Value | Description |
| - | - |
| `WITHDRAW` | Send funds from the Afriex wallet to a recipient's payment method |
| `DEPOSIT` | Receive funds into the Afriex wallet |
| `SWAP` | Convert funds between currencies within the Afriex wallet |
### Transaction Channels
| Channel | Description |
| - | - |
| `BANK_ACCOUNT` | Local bank account transfer |
| `SWIFT` | International SWIFT wire transfer |
| `MOBILE_MONEY` | Mobile money wallet (e.g. M-Pesa, MTN MoMo) |
| `UPI` | Unified Payments Interface (India) |
| `INTERAC` | Interac e-Transfer (Canada) |
| `WE_CHAT` | WeChat Pay |
| `CARD` | Card payment |
| `CRYPTO` | Crypto rail |
| `VIRTUAL_BANK_ACCOUNT` | Hosted virtual bank account |
| `POOL_ACCOUNT` | Shared pool account for incoming deposits |
| `ACH_BANK_ACCOUNT` | US ACH bank transfer |
| `PAYBILL_TILL` | Paybill / Till (M-Pesa business numbers) |
| `RFP` | Request for Payment |
| `VIRTUAL_CARD` | Virtual card |
| `ALIPAY` | Alipay |
| `WALLET` | Afriex wallet (internal leg) |
### Transaction Statuses
| Status | Description |
| - | - |
| `PENDING` | Transaction has been received and is awaiting processing |
| `PROCESSING` | Transaction is actively being processed |
| `SUCCESS` | Transaction settled successfully |
| `FAILED` | Transaction failed. Check `meta.failureReason` for the reason code. See [Failure reasons](#failure-reasons). |
| `CANCELLED` | Transaction was cancelled before processing |
| `REFUNDED` | Funds were returned to the sender |
| `RETRY` | Transaction is being automatically retried |
| `UNKNOWN` | Status could not be determined. Contact support. |
| `SCHEDULED` | Transaction is scheduled to be processed later |
| `CUSTOMER_ACTION_REQUIRED` | Transaction is paused pending an action from the customer |
| `REJECTED` | Transaction was rejected after review |
| `IN_REVIEW` | Transaction is held pending a review |
| `RFI_REQUESTED` | Transaction is held pending a request for information. Someone may contact you for more detail about the transfer. |
| `DISPUTED` | A dispute has been raised against the transaction |
| `DISPUTE_RESOLVED` | The dispute has been resolved |
| `DISPUTE_WON` | The dispute was resolved in your favour |
| `DISPUTE_LOST` | The dispute was resolved against you |
| `DISPUTE_EVIDENCE_SUBMITTED` | Evidence has been submitted for the dispute |
`IN_REVIEW` and `RFI_REQUESTED` are **review states**: the transaction is still in flight and waiting on a review, not on you. Treat them as non-terminal and keep listening, rather than classifying them as failures.
More generally, handle unrecognised statuses as non-terminal rather than failed. New values can be added, and existing ones do not change meaning.
Subscribe to [Transaction Events](/api-reference/endpoint/webhooks/transaction-events) to receive real-time status updates instead of polling the API.
***
## TransactionMeta
The `meta` object passed when creating a transaction. `idempotencyKey` and `reference` are required.
| Field | Type | Required | Description |
| - | - | - | - |
| `idempotencyKey` | string | **Yes** | Unique key to prevent duplicate transactions. Use a UUID or your own unique identifier. |
| `reference` | string | **Yes** | Your internal reference for the transaction (e.g. order ID). Returned on the transaction as `merchantReference`. |
| `narration` | string | No | Human-readable reason or description for the transaction |
| `invoice` | string | Required for SWIFT withdrawals; otherwise No | S3 object key for the invoice document, as returned by [Generate Upload URL](/api-reference/endpoint/media/generate-upload-url). Upload the file to the presigned URL first, then pass the returned `key` here verbatim. The object is verified to exist in storage before the transaction is created; unknown keys are rejected. |
| `settlement` | string | No | Settlement handling. `spot` (default) processes immediately and debits your **Payout wallet**. `request` is WITHDRAW-only; it debits your **Collection wallet** instead and queues the payout for the next business day in the destination payment method's country. Omit `settlement` (or send `spot`) for an ordinary payout. |
```json theme={null}
{
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"reference": "ORDER-12345",
"narration": "Payment for invoice #12345",
"invoice": "64f0c2a1e4b0a1b2c3d4e5f6/invoice.pdf"
}
}
```
Always provide a unique `idempotencyKey` per transaction attempt. If you retry a failed request with the same key, Afriex will return the original transaction instead of creating a duplicate.
### Server-set meta fields
Afriex also sets fields on `meta` after the transaction is created. These echo alongside any fields you passed on create.
| Field | Type | Description |
| - | - | - |
| `otpRequired` | boolean | `true` when the customer must complete an OTP challenge before the transaction can settle. See [Simulating OTP-required deposits](/development#simulating-otp-required-deposits). |
| `failureReason` | object | Present only when `status` is `FAILED` or `REJECTED`. Carries a stable `code`, a customer-safe `message`, and a `retryable` flag. See [Failure reasons](#failure-reasons). |
### Failure reasons
When a transaction reaches `FAILED` or `REJECTED`, `meta.failureReason` explains why using a stable `AFX_*` code and a customer-safe message. Branch on `code` rather than the underlying rail so your integration stays stable across routing changes.
| Field | Type | Description |
| - | - | - |
| `code` | string | Stable `AFX_*` failure code. Safe to switch on; the set grows over time but existing values do not change meaning. |
| `message` | string | Customer-safe short description of the failure. Suitable for display; do not parse. Branch on `code` instead. |
| `retryable` | boolean | `true` when re-submitting the same request (or, for `AFX_PROPOSAL_EXPIRED`, starting a fresh proposal) may succeed. `false` when the caller must change the request before retrying. |
```json theme={null}
{
"meta": {
"failureReason": {
"code": "AFX_VELOCITY_LIMIT_EXCEEDED",
"message": "This transaction exceeds the allowed velocity limit. Try again later.",
"retryable": true
}
}
}
```
| Code | Meaning |
| - | - |
| `AFX_REQUEST_FAILED` | Generic failure while processing the request. Usually retryable. |
| `AFX_SYSTEM_ERROR` | Internal Afriex error. Retry with backoff. |
| `AFX_SERVICE_UNAVAILABLE` | A downstream service was unavailable. Retry with backoff. |
| `AFX_INVALID_CURRENCY` | The requested currency is not supported for this route. |
| `AFX_INVALID_AMOUNT` | The requested amount is invalid or outside allowed limits. |
| `AFX_INVALID_RECIPIENT` | The recipient payment method is invalid or incomplete. |
| `AFX_ACCOUNT_CLOSED` | The recipient account is closed. |
| `AFX_NAME_MISMATCH` | The beneficiary name does not match the account records. |
| `AFX_RECIPIENT_NOT_FOUND` | The recipient account could not be located at the destination institution. |
| `AFX_BENEFICIARY_RESTRICTED` | The beneficiary is restricted and cannot receive funds. |
| `AFX_INVALID_SENDER` | The sender's payment method is invalid or ineligible. |
| `AFX_INVALID_REQUEST` | The request payload is malformed or inconsistent. |
| `AFX_VELOCITY_LIMIT_EXCEEDED` | The customer has exceeded a velocity limit. Retry after the window resets. |
| `AFX_AMOUNT_LIMIT_EXCEEDED` | The request exceeds a per-transaction, daily, or account limit. |
| `AFX_PAYMENT_FAILED` | The payment leg failed at the payment rail. |
| `AFX_COMPLIANCE_REJECTED` | The transaction was rejected during compliance review. |
| `AFX_PROPOSAL_EXPIRED` | The rate proposal expired before the transaction was committed. Start a fresh proposal. |
The `AFX_*` set may grow over time as new failure modes are surfaced. Existing values will not change meaning. Handle unknown codes by falling back to `message`.
***
## PaymentMethod
Empty fields are omitted from responses. Account-shaped channels (`BANK_ACCOUNT`, `MOBILE_MONEY`, `SWIFT`, etc.) populate `accountName`, `accountNumber`, and `institution`; the `CARD` channel instead populates `last4`, `brand`, `expiration`, and `cardName`.
| Field | Type | Description |
| - | - | - |
| `paymentMethodId` | string | Unique identifier for the payment method |
| `customerId` | string | ID of the customer who owns this payment method |
| `channel` | string | Payment channel. See [Payment Channels](#payment-channels) |
| `countryCode` | string | ISO 3166-1 alpha-2 country code |
| `currency` | string | ISO 4217 currency code. Use this instead of inferring from `countryCode`, which is ambiguous (e.g. a USD account in a non-US country) |
| `capabilities` | string\[] | Operations this method is enabled for (e.g. `DEPOSIT`, `WITHDRAW`). Filterable via the `capabilities` query parameter on [List Payment Methods](/api-reference/endpoint/payment-methods/list) |
| `reference` | string | Identifier used to reconcile incoming deposits against |
| `accountName` | string | Account holder name. Present for account-shaped channels |
| `accountNumber` | string | Account number. Present for account-shaped channels |
| `routingNumber` | string | Bank routing number. Present for account-shaped channels that carry one (e.g. ACH bank accounts) |
| `bankAddress` | object | Issuing bank's postal address. See [PaymentMethodBankAddress](#paymentmethodbankaddress) |
| `institution` | object | Bank or provider details. See [PaymentMethodInstitution](#paymentmethodinstitution) |
| `recipient` | object | Recipient contact details. See [PaymentMethodRecipient](#paymentmethodrecipient) |
| `transaction` | object | Per-transaction overrides. See [PaymentMethodTransaction](#paymentmethodtransaction) |
| `status` | string | Lifecycle status. See [Payment Method Statuses](#payment-method-statuses) |
| `last4` | string | Last 4 digits of the card. `CARD` channel only |
| `brand` | string | Card brand (e.g. `Visa`, `MasterCard`). `CARD` channel only |
| `expiration` | object | Card expiry with `month` and `year`. `CARD` channel only |
| `cardName` | string | Name on the card. `CARD` channel only |
| `expiresInMinutes` | integer | Minutes until a dynamic virtual account expires, when applicable |
| `amount` | number | Requested amount for a dynamic virtual account, when applicable |
| `extra` | object | Additional channel-specific properties |
| `meta` | object | Optional metadata attached to the payment method |
### Payment Channels
| Channel | Description |
| - | - |
| `BANK_ACCOUNT` | Local bank account transfer |
| `MOBILE_MONEY` | Mobile money wallet (e.g. M-Pesa, MTN MoMo) |
| `SWIFT` | International SWIFT wire transfer |
| `INTERAC` | Interac e-Transfer (Canada) |
| `UPI` | Unified Payments Interface (India) |
| `WE_CHAT` | WeChat Pay |
| `ALIPAY` | Alipay |
| `CARD` | Card payment |
| `CRYPTO` | Crypto rail |
| `VIRTUAL_BANK_ACCOUNT` | Hosted virtual bank account |
| `POOL_ACCOUNT` | Shared pool account for incoming deposits |
| `ACH_BANK_ACCOUNT` | US ACH bank transfer |
| `PAYBILL_TILL` | Paybill / Till (M-Pesa business numbers) |
| `RFP` | Request for Payment |
| `VIRTUAL_CARD` | Virtual card |
Not every channel is valid when **creating** a payment method. The [Create Payment Method](/api-reference/endpoint/payment-methods/create) endpoint accepts a narrower set of channels; the list above covers all channels that can appear on a returned payment method.
### Payment Method Statuses
| Status | Description |
| - | - |
| `active` | Payment method is active and usable |
| `pending` | Payment method is awaiting activation or verification |
| `deleted` | Payment method has been deleted |
| `expired` | Payment method has expired (e.g. an expired card) |
| `blocked` | Payment method has been blocked |
***
## PaymentMethodRecipient
Recipient contact details attached to a payment method, when supplied.
| Field | Type | Description |
| - | - | - |
| `recipientName` | string | Name of the recipient |
| `recipientEmail` | string (email) | Email address of the recipient |
| `recipientPhone` | string | Phone number of the recipient in E.164 format |
| `recipientAddress` | string | Address of the recipient |
***
## PaymentMethodTransaction
Optional per-transaction details attached to a payment method.
| Field | Type | Description |
| - | - | - |
| `transactionNarration` | string | Optional reason or description for the transaction |
| `transactionInvoice` | string | S3 object key for the invoice document, as returned by [Generate Upload URL](/api-reference/endpoint/media/generate-upload-url) |
***
## PaymentMethodInstitution
| Field | Type | Description |
| - | - | - |
| `institutionId` | string | Unique ID of the bank or mobile money provider |
| `institutionName` | string | Display name of the institution |
| `institutionCode` | string | Institution code (e.g. bank code) |
| `institutionAddress` | string | Physical address of the institution, if required |
| `correspondentBankName` | string | Correspondent (intermediary) bank name. **Mandatory for USD (SWIFT) payout payment methods**; required to produce settlement advices/invoices. |
| `correspondentBankAccountNumber` | string | Correspondent (intermediary) bank account number. **Mandatory for USD (SWIFT) payout payment methods**; required to produce settlement advices/invoices. |
Use the [List Institutions](/api-reference/endpoint/payment-methods/institution) endpoint to retrieve valid institutions for a given country and channel.
***
## PaymentMethodBankAddress
The postal address of the **issuing bank**, not the account holder. Populated per account by the provider that provisioned it, so it is present on virtual accounts and other account-shaped payment methods whose provider supplies it, and omitted otherwise.
| Field | Type | Description |
| - | - | - |
| `line1` | string | First line of the bank's street address |
| `line2` | string | Second line of the bank's street address, when present |
| `city` | string | City |
| `state` | string | State or region, when present |
| `zip` | string | Postal or ZIP code |
| `country` | string | Country, as supplied by the provider |
This is the address to quote as the beneficiary bank when originating an international wire into a virtual account.
On [Create Transaction](/api-reference/endpoint/transactions/create), `correspondentBankName` and `correspondentBankAccountNumber` can also be passed at the top level as a **fallback** when the destination payment method lacks them (USD `WITHDRAW` with `meta.settlement: "request"`). The values stored on the payment method take precedence; the two fields must be supplied together.
***
## PaymentBatch
A saved group of payout recipients funded from one wallet. Add recipients over time, then trigger the payouts in one call with [Withdraw Batch](/api-reference/endpoint/payment-batches/withdraw). Each execution is recorded as a [session](#paymentbatchsession).
| Field | Type | Description |
| - | - | - |
| `id` | string | Unique identifier for the batch |
| `name` | string | Label for the batch |
| `sourcePaymentMethod` | object | The payment method the batch is funded from, as [PaymentMethod](#paymentmethod). Every payout in the batch is converted from this currency. |
| `meta` | object | Optional metadata you attach to the batch |
The list endpoint returns a lighter shape (`id`, `name`, `meta`) without the full `sourcePaymentMethod`; call [Get Payment Batch](/api-reference/endpoint/payment-batches/get) for the full record.
### PaymentBatchRecipient
A recipient inside a batch: a payout target with a fixed amount for each run.
| Field | Type | Description |
| - | - | - |
| `recipientId` | string | The recipient's id within this batch. Use this to update or remove the recipient. |
| `paymentMethodId` | string | The saved payment method backing this recipient (list responses populate this; add/update return the same value in `id`). |
| `channel` | string | Payment channel. See [Payment Channels](#payment-channels). |
| `countryCode` | string | ISO 3166-1 alpha-2 country code |
| `accountName` | string | Account holder name |
| `accountNumber` | string | Account number |
| `institution` | object | Bank / provider details. See [PaymentMethodInstitution](#paymentmethodinstitution). |
| `amount` | object | Per-recipient payout amount for every run of the batch |
### PaymentBatchSession
A single execution of [Withdraw Batch](/api-reference/endpoint/payment-batches/withdraw). Each session records the recipient outcomes for that run.
| Field | Type | Description |
| - | - | - |
| `id` | string | Session (run) id |
| `batchId` | string | The batch this run belongs to |
| `createdAt` | string (ISO 8601) | When the run was started |
| `meta` | object | Per-run metadata, including outcomes |
Bulk operations (bulk add recipients, withdraw) return a **`PaymentBatchOutcomes`** shape with `successes` and `errors` keyed by account number: each entry is either the created object or the reason it failed. Use this to reconcile which items landed.
***
## Error Response
All API errors return a consistent JSON body:
| Field | Type | Description |
| - | - | - |
| `code` | string | Machine-readable error code (e.g. `AUTHENTICATION_ERROR`) |
| `error` | string | Short error category (e.g. `Bad Request`) |
| `details.errorMessage` | string | Specific error reason (e.g. `PHONE_COUNTRY_MISMATCH`) |
| `details.friendlyMessage` | string | User-friendly message you can display to end users |
| `details.data` | object | Optional caller-safe context. On a customer-create uniqueness conflict (`EMAIL_ALREADY_EXISTS`, `PHONE_NUMBER_ALREADY_EXISTS`) contains `{ customerId }` for the existing customer so you can adopt it without a follow-up lookup |
```json theme={null}
{
"code": "INVALID_BUSINESS_CUSTOMER_REQUEST",
"error": "Bad Request",
"details": {
"errorMessage": "PHONE_COUNTRY_MISMATCH",
"friendlyMessage": "The phone number does not match the customer's country."
}
}
```
### Common error codes
| Code | HTTP Status | Description |
| - | - | - |
| `AUTHENTICATION_ERROR` | 401 | API key is missing or invalid |
| `INVALID_BUSINESS_CUSTOMER_REQUEST` | 400 | Customer request validation failed |
| `PHONE_COUNTRY_MISMATCH` | 400 | Phone number country does not match `countryCode` |
| `EMAIL_ALREADY_EXISTS` | 409 | A customer with this email already exists. `details.data.customerId` returns the existing customer's id. |
| `PHONE_NUMBER_ALREADY_EXISTS` | 409 | A customer with this phone number already exists. `details.data.customerId` returns the existing customer's id. |
| `BUSINESS_CUSTOMER_NOT_FOUND` | 404 | No customer with that id belongs to your business |
| `BUSINESS_PAYMENT_METHOD_NOT_FOUND` | 404 | No payment method with that id belongs to your business |
| `BUSINESS_TRANSACTION_NOT_FOUND` | 404 | No transaction with that id belongs to your business |
| `VALIDATION_ERROR` | 422 | Request failed schema validation. `details.friendlyMessage` names the offending field |
| `RATE_LIMIT_ERROR` | 429 | Too many requests. Slow down and retry with backoff. |
| `INTERNAL_SERVER_ERROR` | 500 | Unexpected server error. Safe to retry idempotent requests with backoff |
| `SERVICE_UNAVAILABLE` | 503 | An upstream processor failed or timed out, or the API is momentarily offline. Retry with backoff and honour `Retry-After` when present |
There is no generic `NOT_FOUND` code. Missing resources return a specific code naming the resource, as above.
**The API never returns 502 or 504.** Gateway failures (an upstream processor timing out, or the API being momentarily offline) are sent as **503** so the JSON body survives; `500` and `503` are passed through to you untouched. If you do receive a `502` or `504`, it came from an intermediary such as a proxy or CDN, not from Afriex, and the body will not be an Afriex error envelope.
# Permissions
Source: https://docs.afriex.com/api-reference/permissions
The permission each Afriex Business API endpoint requires, plus how keys and permissions relate.
Every API key is scoped to a configurable set of **permissions** chosen when the key is created in the [Dashboard](https://business.afriex.com/). Requests are rejected with `401 Unauthorized` when the key does not carry the permission the target endpoint requires, so pick the minimum set your integration needs.
Keys created **before** permissions existed carry no permissions and keep access to every endpoint, so no action is required for older keys. Provision a fresh key from the dashboard to opt into the scoped model.
## How the check works
* **Header:** requests carry the key in `x-api-key` (see [Authentication](/api-reference/introduction#authentication)).
* **Multiple permissions:** where several are listed for one endpoint, **any one** of them is enough.
* **Failure mode:** a key missing the required permission is rejected with `401 Unauthorized`, exactly the same shape as an unrecognised, malformed, revoked, or disabled key. The API does not distinguish those cases on the wire.
## Endpoint permission reference
| Endpoint | Permission |
| - | - |
| `GET /customer`, `GET /customer/{customerId}` | `CUSTOMER.READ` |
| `POST /customer` | `CUSTOMER.CREATE` |
| `PATCH /customer/{customerId}`, `PATCH /customer/{customerId}/kyc`, `POST /customer/{customerId}/verify` | `CUSTOMER.UPDATE` |
| `DELETE /customer/{customerId}` | `CUSTOMER.DELETE` |
| `GET /payment-method`, `GET /payment-method/{paymentMethodId}`, `GET /payment-method/institution`, `GET /payment-method/institution/codes`, `GET /payment-method/resolve`, `GET /payment-method/virtual-account`, `GET /payment-method/pool-account` | `PAYMENT_METHOD.READ` |
| `POST /payment-method`, `DELETE /payment-method/{paymentMethodId}`, `POST /payment-method/virtual-account`, `GET /payment-method/crypto-wallet`, `POST /payment-method/virtual-account/simulate-transfer` *(sandbox only)* | `PAYMENT_METHOD.CREATE` |
| `POST /transaction` | `TRANSACTION.DEPOSIT.CREATE`, `TRANSACTION.WITHDRAW.CREATE` or `TRANSACTION.SWAP.CREATE` |
| `GET /transaction/quote` | `TRANSACTION.WITHDRAW.CREATE` |
| `POST /transaction/{transactionId}/authorize`, `POST /transaction/pool-account` | `TRANSACTION.DEPOSIT.CREATE` |
| `POST /transaction/{transactionId}/simulate` *(sandbox only)* | `TRANSACTION.DEPOSIT.CREATE` or `TRANSACTION.WITHDRAW.CREATE` |
| `GET /transaction`, `GET /transaction/{transactionId}`, `GET /transaction/{transactionId}/advice` | `TRANSACTION.HISTORY.READ` |
| `GET /org/balance`, `GET /org/rates` | `WALLET.BALANCES.READ` |
| `POST /media/url` | `PAYMENT_METHOD.CREATE` |
| `POST /sme-registration` | `COMPLIANCE.KYB.SUBMIT` |
| `GET /sme-registration/status` | `COMPLIANCE.KYB.READ` |
| `POST /checkout-session` | `CHECKOUT_LINK.CREATE` |
| `GET /payment-batch`, `GET /payment-batch/{batchId}`, `GET /payment-batch/{batchId}/recipients` | `PAYMENT_METHOD.READ` |
| `POST /payment-batch`, `PATCH /payment-batch/{batchId}`, `DELETE /payment-batch/{batchId}`, `POST /payment-batch/{batchId}/recipients`, `POST /payment-batch/{batchId}/recipients/bulk`, `PATCH /payment-batch/{batchId}/recipients/{recipientId}`, `DELETE /payment-batch/{batchId}/recipients/{recipientId}` | `PAYMENT_METHOD.CREATE` |
| `POST /payment-batch/{batchId}/withdraw` | `TRANSACTION.WITHDRAW.CREATE` |
| `GET /payment-batch/{batchId}/sessions` | `TRANSACTION.HISTORY.READ` |
| `POST /org/balance/topup`, `POST /webhooks/trigger` | None; any valid key |
Managing keys? Head to **Developer → API keys** in the [Dashboard](https://business.afriex.com/) to create, rotate, or revoke them.
# Supported Currencies & Payment Rails
Source: https://docs.afriex.com/api-reference/supported-currencies
Complete list of countries, currencies, and payment channels available through the Afriex Business API
Afriex routes payments across **30+ currencies** and **100+ countries**. Use this page to confirm which payment rails are available for a given country before building your integration.
Local and international currencies
Coverage across 5 continents
Bank, mobile money, SWIFT, crypto, and more
Collections and payouts both supported
## Rail Reference
| Rail | API `channel` value | Description |
| - | - | - |
| Bank Account | `BANK_ACCOUNT` | Local bank account transfer |
| Virtual Account | `VIRTUAL_BANK_ACCOUNT` | System-generated account number for inbound deposits |
| Pool Account | `POOL_ACCOUNT` | Shared pool account for multiple users |
| Mobile Money | `MOBILE_MONEY` | Mobile money wallet (M-Pesa, MTN MoMo, Airtel, etc.) |
| SWIFT | `SWIFT` | International SWIFT wire transfer |
| Interac | `INTERAC` | Interac e-Transfer (Canada) |
| UPI | `UPI` | Unified Payments Interface (India) |
| WeChat Pay | `WE_CHAT` | WeChat Pay (China) |
| Alipay | `ALIPAY` | Alipay (China) |
| Crypto | `CRYPTO` | Crypto wallet (USDC / USDT on supported networks) |
| Paybill / Till | `PAYBILL_TILL` | M-Pesa Paybill or Till number (Kenya). Not currently creatable through the API |
***
## Supported Countries
| Country | Currency | Deposit Rails | Payout Rails |
| - | - | - | - |
| 🇧🇯 Benin | XOF | `Mobile Money` | `Mobile Money` |
| 🇧🇼 Botswana | BWP | — | `Mobile Money` |
| 🇧🇫 Burkina Faso | XOF | `Mobile Money` *(coming soon)* | `Mobile Money` *(coming soon)* |
| 🇨🇲 Cameroon | XAF | `Mobile Money` | `Bank Account` `Mobile Money` |
| 🇨🇫 Central African Republic | XAF | — | `Mobile Money` *(coming soon)* |
| 🇨🇬 Republic of Congo | XAF | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇨🇮 Côte d'Ivoire | XOF | `Mobile Money` | `Bank Account` `Mobile Money` |
| 🇨🇩 DR Congo | CDF | — | `Mobile Money` *(coming soon)* |
| 🇪🇬 Egypt | EGP | — | `Bank Account` `Mobile Money` |
| 🇪🇹 Ethiopia | ETB | `Mobile Money` | `Bank Account` `Mobile Money` |
| 🇬🇦 Gabon | XAF | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇬🇲 Gambia | GMD | — | `Mobile Money` |
| 🇬🇭 Ghana | GHS | `Mobile Money` *(coming soon)* `Virtual Account` | `Bank Account` `Mobile Money` |
| 🇬🇳 Guinea | GNF | — | `Mobile Money` |
| 🇬🇼 Guinea-Bissau | XOF | — | `Mobile Money` |
| 🇰🇪 Kenya | KES | `Mobile Money` `Virtual Account` | `Bank Account` `Mobile Money` `Paybill / Till` |
| 🇲🇬 Madagascar | MGA | — | `Mobile Money` |
| 🇲🇼 Malawi | MWK | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇲🇱 Mali | XOF | — | `Mobile Money` *(coming soon)* |
| 🇲🇦 Morocco | MAD | — | `Mobile Money` *(coming soon)* |
| 🇲🇿 Mozambique | MZN | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇳🇬 Nigeria | NGN | `Virtual Account` | `Bank Account` |
| 🇷🇼 Rwanda | RWF | `Mobile Money` *(coming soon)* | `Bank Account` `Mobile Money` |
| 🇸🇳 Senegal | XOF | — | `Bank Account` `Mobile Money` |
| 🇸🇱 Sierra Leone | SLE | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇿🇦 South Africa | ZAR | — | `Bank Account` |
| 🇸🇸 South Sudan | SSP | — | `Mobile Money` *(coming soon)* |
| 🇹🇿 Tanzania | TZS | `Mobile Money` | `Mobile Money` |
| 🇹🇬 Togo | XOF | — | `Mobile Money` *(coming soon)* |
| 🇺🇬 Uganda | UGX | `Mobile Money` | `Bank Account` `Mobile Money` |
| 🇿🇲 Zambia | ZMW | `Mobile Money` *(coming soon)* | `Mobile Money` |
| Country | Currency | Deposit Rails | Payout Rails |
| - | - | - | - |
| 🇨🇦 Canada | CAD | `Interac` *(coming soon)* | `Interac` *(coming soon)* |
| 🇺🇸 United States | USD | `Virtual Account` | `Bank Account` `SWIFT` |
USD `SWIFT` payouts route to **100 countries** globally — not just the US. See [SWIFT Coverage](#swift-coverage) below for the full country list.
| Country | Currency | Deposit Rails | Payout Rails |
| - | - | - | - |
| 🇦🇹 Austria | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇧🇪 Belgium | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇧🇬 Bulgaria | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇭🇷 Croatia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇨🇾 Cyprus | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇨🇿 Czech Republic | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇩🇰 Denmark | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇪🇪 Estonia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇫🇮 Finland | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇫🇷 France | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇩🇪 Germany | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇬🇷 Greece | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇭🇺 Hungary | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇮🇪 Ireland | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇮🇹 Italy | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇱🇻 Latvia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇱🇹 Lithuania | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇱🇺 Luxembourg | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇲🇹 Malta | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇳🇱 Netherlands | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇳🇴 Norway | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇵🇱 Poland | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇵🇹 Portugal | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇷🇴 Romania | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇸🇰 Slovakia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇸🇮 Slovenia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇪🇸 Spain | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇸🇪 Sweden | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇺🇦 Ukraine | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇬🇧 United Kingdom | GBP | `Virtual Account` *(coming soon)* | `Bank Account` |
EUR bank payouts use the **SEPA** network and require a valid IBAN. GBP payouts use **Faster Payments**.
| Country | Currency | Deposit Rails | Payout Rails |
| - | - | - | - |
| 🇨🇳 China | CNY | — | `Bank Account` `WeChat Pay` `Alipay` |
| 🇮🇳 India | INR | — | `Bank Account` `UPI` |
| 🇵🇰 Pakistan | PKR | — | `Bank Account` `Mobile Money` |
CNY `Bank Account` payouts require the recipient's **phone number**, **ID type**, and **ID number** in addition to standard bank details.
| Asset | Supported Countries | Deposit Rails | Payout Rails |
| - | - | - | - |
| USDC | 🌍 100+ countries globally | `Crypto` | — |
| USDT | 🌍 100+ countries globally | `Crypto` | — |
USDC and USDT deposits are accepted from 100+ countries via crypto wallet.
***
## Coming Soon
**Pool Accounts** are live. Fetch your business's shared pool account for a country with [Get Pool Account](/api-reference/endpoint/payment-methods/pool-account), then submit proof of an inbound payment with [Submit Pool Account Payment Proof](/api-reference/endpoint/transactions/pool-account-proof). Country-level coverage is still expanding.
***
## SWIFT Coverage
USD `SWIFT` payouts (`channel: SWIFT`) are available to **100 countries** — far beyond the US alone. This makes SWIFT the default choice for international USD disbursements when no local rail is available.
SWIFT transfers require: recipient email, recipient address, SWIFT/BIC code, and a transaction invoice.
| Region | Countries |
| - | - |
| **Africa** | 🇩🇿 Algeria · 🇧🇯 Benin · 🇧🇼 Botswana · 🇧🇫 Burkina Faso · 🇨🇲 Cameroon · 🇨🇫 Central African Republic · 🇨🇬 Republic of Congo · 🇨🇮 Côte d'Ivoire · 🇨🇩 DR Congo · 🇪🇬 Egypt · 🇪🇹 Ethiopia · 🇬🇦 Gabon · 🇬🇲 Gambia · 🇬🇭 Ghana · 🇬🇳 Guinea · 🇬🇼 Guinea-Bissau · 🇰🇪 Kenya · 🇲🇬 Madagascar · 🇲🇼 Malawi · 🇲🇱 Mali · 🇲🇦 Morocco · 🇲🇿 Mozambique · 🇳🇦 Namibia · 🇳🇪 Niger · 🇳🇬 Nigeria · 🇷🇼 Rwanda · 🇸🇳 Senegal · 🇸🇱 Sierra Leone · 🇿🇦 South Africa · 🇸🇸 South Sudan · 🇹🇿 Tanzania · 🇹🇬 Togo · 🇹🇳 Tunisia · 🇺🇬 Uganda · 🇿🇲 Zambia · 🇿🇼 Zimbabwe |
| **Americas** | 🇦🇷 Argentina · 🇧🇷 Brazil · 🇨🇦 Canada · 🇨🇱 Chile · 🇨🇴 Colombia · 🇭🇹 Haiti · 🇲🇽 Mexico · 🇵🇪 Peru · 🇺🇸 United States · 🇺🇾 Uruguay |
| **Europe** | 🇦🇹 Austria · 🇧🇪 Belgium · 🇧🇬 Bulgaria · 🇭🇷 Croatia · 🇨🇾 Cyprus · 🇨🇿 Czech Republic · 🇩🇰 Denmark · 🇪🇪 Estonia · 🇫🇮 Finland · 🇫🇷 France · 🇩🇪 Germany · 🇬🇮 Gibraltar · 🇬🇷 Greece · 🇭🇺 Hungary · 🇮🇪 Ireland · 🇮🇹 Italy · 🇱🇻 Latvia · 🇱🇮 Liechtenstein · 🇱🇹 Lithuania · 🇱🇺 Luxembourg · 🇲🇹 Malta · 🇳🇱 Netherlands · 🇳🇴 Norway · 🇵🇱 Poland · 🇵🇹 Portugal · 🇷🇴 Romania · 🇷🇺 Russia · 🇸🇰 Slovakia · 🇸🇮 Slovenia · 🇪🇸 Spain · 🇸🇪 Sweden · 🇨🇭 Switzerland · 🇺🇦 Ukraine · 🇬🇧 United Kingdom |
| **Asia & Pacific** | 🇦🇺 Australia · 🇧🇩 Bangladesh · 🇨🇳 China · 🇭🇰 Hong Kong · 🇮🇩 Indonesia · 🇮🇳 India · 🇯🇵 Japan · 🇰🇷 South Korea · 🇰🇼 Kuwait · 🇲🇾 Malaysia · 🇳🇿 New Zealand · 🇵🇭 Philippines · 🇵🇰 Pakistan · 🇶🇦 Qatar · 🇸🇦 Saudi Arabia · 🇸🇬 Singapore · 🇹🇭 Thailand · 🇹🇷 Turkey · 🇦🇪 UAE · 🇻🇳 Vietnam |
Use the [List Institutions](/api-reference/endpoint/payment-methods/institution) endpoint with `channel=SWIFT` to look up valid SWIFT/BIC codes for a given recipient country.
# Integration Guide
Source: https://docs.afriex.com/development
Best practices for integrating the Afriex Business API
## Integration Guide
This guide covers best practices for integrating the Afriex Business API into your application.
Get the full OpenAPI spec at this endpoint for code generation, SDKs, and API tooling.
Add the Afriex Business API to your MCP client and search the docs from Cursor, Claude Code, and other MCP-enabled tools.
## Environments
The Afriex API provides two environments:
| Environment | Base URL | Purpose |
| - | - | - |
| Staging | `https://sandbox.api.afriex.com` | Testing and development |
| Production | `https://api.afriex.com` | Live transactions |
**Never use production API keys in your testing environment.**
## Authentication
All API requests require authentication using your API key in the `x-api-key` header:
```bash theme={null}
curl -H "x-api-key: YOUR_API_KEY" \
https://sandbox.api.afriex.com/api/v1/customer
```
## Idempotency
For critical operations like creating transactions, use idempotency keys to prevent duplicate processing:
```json theme={null}
{
"customerId": "CUSTOMER_ID",
"type": "WITHDRAW",
"sourceAmount": "10",
"destinationAmount": 5000,
"sourceCurrency": "USD",
"destinationCurrency": "NGN",
"destinationId": "PAYMENT_METHOD_ID",
"meta": {
"idempotencyKey": "unique-key-123",
"reference": "YOUR_REFERENCE"
}
}
```
## Error Handling
The API uses standard HTTP status codes:
| Status | Meaning |
| - | - |
| 200 | Success |
| 201 | Created |
| 204 | No Content - Successful deletion, no body returned |
| 400 | Bad Request - Check your request payload |
| 401 | Unauthorized - Invalid or missing API key |
| 404 | Not Found - Resource doesn't exist |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Server Error - Contact support |
### Error response format
All error responses follow a consistent structure:
```json theme={null}
{
"code": "INVALID_BUSINESS_CUSTOMER_REQUEST",
"error": "Bad Request",
"details": {
"errorMessage": "PHONE_COUNTRY_MISMATCH",
"friendlyMessage": "The phone number does not match the customer's country."
}
}
```
| Field | Description |
| - | - |
| `code` | Machine-readable error code for programmatic handling |
| `error` | Short human-readable error category |
| `details.errorMessage` | Specific reason for the error |
| `details.friendlyMessage` | User-friendly message suitable for displaying to end users |
Use the `details.friendlyMessage` field to surface errors directly to your users, and use `code` for programmatic error handling in your application logic.
## Pagination
List endpoints support pagination with `page` and `limit` parameters:
```bash theme={null}
curl -H "x-api-key: YOUR_API_KEY" \
"https://sandbox.api.afriex.com/api/v1/customer?page=0&limit=10"
```
| Parameter | Type | Default | Description |
| - | - | - | - |
| `page` | integer | `0` | Page number, zero-indexed (first page = `0`) |
| `limit` | integer | `10` | Number of results per page (max `100`) |
All list endpoints return results in this shape:
```json theme={null}
{
"page": 0,
"total": 20,
"data": []
}
```
Use the `total` field to calculate the number of pages: `Math.ceil(total / limit)`.
## Webhook Integration
Set up webhooks to receive real-time event notifications:
1. Configure your webhook URL in the [Dashboard](https://business.afriex.com/)
2. Verify the `x-webhook-signature` header (ECDSA P-384 with SHA-256, DER signature, base64) using the **raw** request body. The key is not RSA. See [Security and signature validation](/api-reference/endpoint/webhooks/introduction#security-and-signature-validation)
3. Use the webhook public key from **Dashboard -> Developers -> Webhooks** (staging and production keys are different)
4. Return `200` quickly after successful verification and processing
If signature verification fails, return `400` or `401`. Afriex retries failed deliveries up to 12 times with exponential backoff (starting at 30 seconds).
Learn more about webhook setup, security, and event types.
## Rate Limiting
Be mindful of rate limits when making API calls:
* Implement exponential backoff for retries
* Cache exchange rates and other static data
* Use webhooks instead of polling for status updates
## Testing Your Integration
Use the staging environment to test all API calls without affecting live data.
Test webhook handling with the staging environment events.
Switch to production base URL and API keys when ready.
### Building and testing in sandbox
For the full sandbox reference, including the test-value taxonomy, how to simulate deterministic outcomes, OTP flow, virtual-account transfers, balance topups, and webhook drills, see the [Testing in Sandbox guide](/guides/testing-in-sandbox).
## Support
Need help with your integration? Contact us at [support@afriex.com](mailto:support@afriex.com).
# Connecting the Pieces
Source: https://docs.afriex.com/guides/connecting-the-pieces
How Customers, Payment Methods, and Transactions link together
## What this guide covers
If you've read the quickstart but aren't sure exactly *which ID goes where*, or why you need to create three different things before money can move, this guide is for you.
The Afriex Business API has three core building blocks. They must be created in order, and each one produces an ID that the next step needs. This guide explains what each building block is, what ID it gives you, and exactly where that ID is used.
***
## The big picture
Think of it like this: a **Customer** is the person your business is acting on behalf of (the sender or originator). A **Payment Method** holds the account details of the person being paid (the recipient). A **Transaction** is the instruction to move money, and it needs to know *on whose behalf* you are acting and *which recipient account* to use.
Here is how the IDs connect:
```mermaid theme={null}
flowchart TD
A["Step 1: Create Customer
POST /api/v1/customer
─────────────────────────────
✦ Returns: customerId"]
B["Step 2: Create Payment Method
POST /api/v1/payment-method
─────────────────────────────
↳ Input: customerId
✦ Returns: paymentMethodId"]
C["Step 3: Create Transaction
POST /api/v1/transaction
─────────────────────────────
↳ Input: customerId
↳ Input: destinationId = paymentMethodId (WITHDRAW)
↳ Input: sourceId = paymentMethodId (DEPOSIT)
✦ Returns: transactionId"]
A -->|"customerId"| B
B -->|"paymentMethodId"| C
A -->|"customerId"| C
```
### ID quick-reference
| ID field | Belongs to | Used in |
| - | - | - |
| `customerId` | Customer | Payment Method creation, Transaction creation |
| `paymentMethodId` | Payment Method | Transaction as `destinationId` or `sourceId` |
| `destinationId` | Transaction field | Equals a `paymentMethodId` (used in WITHDRAW) |
| `sourceId` | Transaction field | Equals a `paymentMethodId` (used in DEPOSIT) |
| `transactionId` | Transaction | Status lookups, webhook events |
`destinationId` and `sourceId` in a Transaction are both **Payment Method IDs**. They are not special IDs. They are exactly the `paymentMethodId` value returned when you created the Payment Method. This is the single most common point of confusion.
***
## Each building block explained
### Customer
**What it is:** A Customer is the person or business your business is acting on behalf of. They are the originator or sender of the transaction, not the recipient. For example, if your platform processes payroll for a company, each employee whose salary you send out is a Customer. The Customer record stores their identity information (name, email, phone, country) and is used to attribute transactions to the right person in your system.
**What ID it gives you:** `customerId`
**When you use that ID:**
* Pass it as `customerId` when creating a Payment Method (to link the account to this person)
* Pass it as `customerId` when creating a Transaction (to identify whose account is involved)
**Fields to create a Customer:**
| Field | Required | Example |
| - | - | - |
| `fullName` | Yes | `"Amara Osei"` |
| `email` | Yes | `"amara@example.com"` |
| `phone` | Yes | `"+233201234567"` (E.164 format) |
| `countryCode` | Yes | `"GH"` (ISO 3166-1 alpha-2) |
***
### Payment Method
**What it is:** A Payment Method holds the account details of the person being paid or collected from: their bank account, mobile money wallet, or other financial account. It is linked to a Customer (the originator on whose behalf you are acting), but the account details it contains belong to the end recipient. You need a Payment Method before you can send or receive money, because the Transaction needs to know exactly *which account* to credit or debit.
**What ID it gives you:** `paymentMethodId`
**When you use that ID:**
* As `destinationId` in a WITHDRAW transaction (the account the money is sent to)
* As `sourceId` in a DEPOSIT transaction (the account the money is pulled from)
A Payment Method has a `type` field, either `WITHDRAW` (used to send funds to a recipient) or `DEPOSIT` (used to collect funds from a customer). Make sure you create the right type for the transaction you plan to run.
**Supported channels:**
| Channel | Description | Supports | Common use case |
| - | - | - | - |
| `BANK_ACCOUNT` | Local bank transfer | Withdraw | Payout to Nigerian GTBank, Kenyan KCB |
| `MOBILE_MONEY` | Mobile wallet | Deposit & Withdraw | MTN MoMo, M-Pesa, Airtel Money |
| `SWIFT` | International wire transfer | Withdraw | Cross-border payouts to Europe, US |
| `UPI` | Unified Payments Interface | Withdraw | Payouts to India |
| `INTERAC` | Interac e-Transfer | Deposit & Withdraw | Collections and payouts in Canada |
| `WE_CHAT` | WeChat Pay and Alipay | Withdraw | Payouts to China |
| `VIRTUAL_BANK_ACCOUNT` | Afriex-issued virtual account | Deposit | Receive collections in NGN, GHS, KES, and USD |
| `POOL_ACCOUNT` | Shared pool account | Deposit | Shared collection account (production only) |
| `CRYPTO_WALLET` | Crypto wallet | Deposit | USDC / USDT deposits |
#### Virtual bank accounts
`VIRTUAL_BANK_ACCOUNT` is not created through the standard `POST /api/v1/payment-method` endpoint. It has two dedicated endpoints, and they do different things:
* **[Create Virtual Account](/api-reference/endpoint/payment-methods/virtual-account-create)** (`POST /api/v1/payment-method/virtual-account`) creates one. Pass `currency`, optionally `customerId`, and exactly one of `label` or `amount`.
* **[List Virtual Accounts](/api-reference/endpoint/payment-methods/virtual-account)** (`GET /api/v1/payment-method/virtual-account`) is **read-only**. It returns every active account for that customer and currency, and responds `200` with `data: []` when none exist. It does **not** create one.
The `GET` endpoint used to create an account on the fly when none existed. It no longer does. If you are relying on that behavior, calling `GET` now returns an empty list rather than a new account, with no error. Call `POST` to create.
The two account shapes are chosen by which field you pass to `POST`:
* **Static (`label`):** A permanent virtual bank account. The same account number is returned on every subsequent list for that customer and currency, and the customer can fund it at any time for any amount. `label` is one of `SALES`, `OPERATIONS`, `PAYROLL`, `COLLECTIONS`, `VENDOR_PAYMENTS`, `TAX`, `REFUNDS`, `MARKETING`, `TREASURY`, `GENERAL`.
* **Dynamic (`amount`):** An account tied to a specific amount that expires after a set window (the response includes `expiresInMinutes`). Use this when collecting a specific payment within a defined time.
| | Static | Dynamic |
| - | - | - |
| Triggered by | Passing `label` | Passing `amount` |
| Expires | Never | Yes (see `expiresInMinutes` in response) |
| Amount-specific | No | Yes |
When you pass a `customerId`, virtual accounts are currently restricted to **NGN**. Other currencies for a specific customer are rejected with `UNSUPPORTED_VIRTUAL_ACCOUNT_CURRENCY`. There is also a cap on how many accounts a single business-customer-currency combination can hold; exceeding it returns `VIRTUAL_ACCOUNT_LIMIT_REACHED`.
**POOL\_ACCOUNT** returns the business's shared pool account for a country. Requires `country`; pass `customerId` to scope the response to a specific end-user. Use the `reference` on the response to reconcile incoming deposits.
Virtual accounts are for your **end customers**. Link each one to a `customerId` so payments are attributed to the right person.
***
### Transaction
**What it is:** A Transaction is the actual instruction to move money. It references a Customer and (depending on the type) one of their Payment Methods.
**What ID it gives you:** `transactionId`
**When you use that ID:**
* To check the status: `GET /api/v1/transaction/{transactionId}`
* It appears in webhook events so you can match notifications to records in your system
**Three transaction types:**
| Type | What it does | Payment method needed |
| - | - | - |
| `WITHDRAW` | Send money *from* your Afriex wallet *to* a customer's account | `destinationId` (= paymentMethodId) |
| `DEPOSIT` | Pull money *from* a customer's account *into* your Afriex wallet | `sourceId` (= paymentMethodId) |
| `SWAP` | Convert currency *within* your Afriex wallet | None |
**WITHDRAW:** Your business wallet is the implicit source. Afriex debits your wallet automatically. You do not specify a source payment method.
**DEPOSIT:** Your business wallet is the implicit destination. Afriex credits your wallet automatically. You do not specify a destination payment method.
**SWAP:** Both source and destination are your business wallet. No customer or payment method IDs are needed at all.
***
## The 3-step integration sequence
Here is the exact order of operations, with the IDs flowing from one step to the next.
Register the person on whose behalf the transaction is being made (the originator or sender).
```bash theme={null}
curl -X POST https://sandbox.api.afriex.com/api/v1/customer \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"fullName": "Amara Osei",
"email": "amara@example.com",
"phone": "+233201234567",
"countryCode": "GH"
}'
```
Save the `customerId` from the response. You need it in the next step.
```json theme={null}
{
"data": {
"customerId": "69516dd0464b2213bd74cfad",
"name": "Amara Osei",
"email": "amara@example.com",
"phone": "+233201234567",
"countryCode": "GH"
}
}
```
Register the recipient's bank account or mobile wallet (the account the money will be sent to or collected from). Link it to the Customer from Step 1 by passing their `customerId`.
First, look up the correct institution code for the customer's bank or mobile provider:
```bash theme={null}
curl "https://sandbox.api.afriex.com/api/v1/payment-method/institution?channel=MOBILE_MONEY&countryCode=GH" \
-H "x-api-key: YOUR_API_KEY"
```
Then create the payment method using the `institutionCode` and `institutionName` from that response:
```bash theme={null}
curl -X POST https://sandbox.api.afriex.com/api/v1/payment-method \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "MOBILE_MONEY",
"accountName": "Amara Osei",
"accountNumber": "0201234567",
"countryCode": "GH",
"institution": {
"institutionCode": "MTN",
"institutionName": "MTN"
}
}'
```
Save the `paymentMethodId` from the response. This is what goes into `destinationId` (or `sourceId`) in your transaction.
```json theme={null}
{
"data": {
"paymentMethodId": "690df3281c11eea59108fcaf",
"customerId": "69516dd0464b2213bd74cfad",
"channel": "MOBILE_MONEY",
"accountName": "Amara Osei",
"accountNumber": "0201234567",
"countryCode": "GH"
}
}
```
Now create the transaction using both IDs from the previous steps.
* `customerId` = the ID from Step 1
* `destinationId` = the `paymentMethodId` from Step 2 (for a WITHDRAW)
```bash theme={null}
curl -X POST https://sandbox.api.afriex.com/api/v1/transaction \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"sourceAmount": "10",
"destinationAmount": "91.50",
"sourceCurrency": "USD",
"destinationCurrency": "GHS",
"destinationId": "690df3281c11eea59108fcaf",
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440001",
"reference": "ORDER-98765"
}
}'
```
The response includes a `transactionId`. Use it to track status or match incoming webhook events.
***
## Transaction type payloads side by side
```json WITHDRAW: Pay out to a customer's account theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"sourceAmount": "10",
"destinationAmount": "16500",
"sourceCurrency": "USD",
"destinationCurrency": "NGN",
"destinationId": "690df3281c11eea59108fcaf",
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440001",
"reference": "ORDER-001"
}
}
```
```json DEPOSIT: Collect from a customer's account theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "DEPOSIT",
"sourceAmount": "500",
"destinationAmount": "12",
"sourceCurrency": "KES",
"destinationCurrency": "USD",
"sourceId": "690df3281c11eea59108fcc1",
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440002",
"reference": "COLLECTION-001"
}
}
```
```json SWAP: Convert currency in your wallet theme={null}
{
"type": "SWAP",
"sourceAmount": "500",
"sourceCurrency": "USD",
"destinationCurrency": "NGN",
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440003",
"reference": "SWAP-001"
}
}
```
### What changes between each type
| Field | WITHDRAW | DEPOSIT | SWAP |
| - | - | - | - |
| `customerId` | Required | Required | Not required |
| `destinationId` | Required (= paymentMethodId) | Not used | Not used |
| `sourceId` | Not used | Required (= paymentMethodId) | Not used |
| `destinationAmount` | One of `sourceAmount` / `destinationAmount` | One of `sourceAmount` / `destinationAmount` | Optional (API calculates it at live rate) |
| `meta.idempotencyKey` | Required | Required | Required |
| `meta.reference` | Required | Required | Required |
***
## Payment method fields by channel
Different payment channels require different fields. Use the sections below to find the exact payload shape for your channel.
`customerId` is listed below for every channel because that is the usual case, but it is **optional**. Omit it and the payment method is attached to the business owner instead of an end customer. Pass it whenever the method belongs to one of your customers, which is almost always what you want.
Required fields: `customerId`, `channel`, `accountName`, `accountNumber`, `countryCode`, `institution.institutionCode`, `institution.institutionName`
Look up valid institution codes first: [List Institutions](/api-reference/endpoint/payment-methods/institution)
You can also verify the account holder name before creating: [Resolve Payment Method](/api-reference/endpoint/payment-methods/resolve)
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "BANK_ACCOUNT",
"accountName": "John Doe",
"accountNumber": "0123456789",
"countryCode": "NG",
"institution": {
"institutionCode": "058",
"institutionName": "GTBank"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (the mobile number), `countryCode`, `institution.institutionCode`, `institution.institutionName`
Look up valid institution codes for the country: [List Institutions](/api-reference/endpoint/payment-methods/institution)
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "MOBILE_MONEY",
"accountName": "Amara Osei",
"accountNumber": "0201234567",
"countryCode": "GH",
"institution": {
"institutionCode": "MTN",
"institutionName": "MTN"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (IBAN or account number), `countryCode`, `institution.institutionCode` (SWIFT/BIC), `institution.institutionName`, `institution.institutionAddress`, `recipient.recipientAddress`
Use [Institution Codes](/api-reference/endpoint/payment-methods/institution-codes) to resolve a SWIFT code to a bank name.
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "SWIFT",
"accountName": "John Doe",
"accountNumber": "DE89370400440532013000",
"countryCode": "DE",
"institution": {
"institutionCode": "DEUTDEDB",
"institutionName": "Deutsche Bank",
"institutionAddress": "Taunusanlage 12, 60262 Frankfurt am Main, Germany"
},
"recipient": {
"recipientAddress": "Musterstrasse 1, 10115 Berlin, Germany"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (UPI ID / VPA), `countryCode`, `institution.institutionCode` (`"UPI"`), `institution.institutionName` (`"UPI"`), `recipient.recipientPhone`
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "UPI",
"accountName": "Raj Kumar",
"accountNumber": "rajkumar@upi",
"countryCode": "IN",
"institution": {
"institutionCode": "UPI",
"institutionName": "UPI"
},
"recipient": {
"recipientPhone": "+919876543210"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (the email address registered for Interac), `countryCode`, `institution.institutionCode` (`"INTERAC"`), `institution.institutionName` (`"INTERAC"`), `recipient.recipientEmail`
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "INTERAC",
"accountName": "John Doe",
"accountNumber": "john.doe@email.com",
"countryCode": "CA",
"institution": {
"institutionCode": "INTERAC",
"institutionName": "INTERAC"
},
"recipient": {
"recipientEmail": "john.doe@email.com"
}
}
```
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (phone number), `countryCode`, `institution.institutionCode` (`"WECHAT"`), `institution.institutionName` (`"WECHAT"`), `recipient.recipientPhone`
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "WE_CHAT",
"accountName": "Zhang Wei",
"accountNumber": "+8613812345678",
"countryCode": "CN",
"institution": {
"institutionCode": "WECHAT",
"institutionName": "WECHAT"
},
"recipient": {
"recipientPhone": "+8613812345678"
}
}
```
Alipay does not have a channel of its own. Create it on the `WE_CHAT` channel and set `institution.institutionName` to `ALIPAY`; sending `"channel": "ALIPAY"` is rejected.
Required fields: `customerId`, `channel`, `accountName`, `accountNumber` (Alipay-linked phone number or account ID), `countryCode`, `institution.institutionName`
```json theme={null}
{
"customerId": "69516dd0464b2213bd74cfad",
"type": "WITHDRAW",
"channel": "WE_CHAT",
"accountName": "Zhang Wei",
"accountNumber": "+8613812345678",
"countryCode": "CN",
"institution": {
"institutionName": "ALIPAY"
}
}
```
Virtual bank accounts are not created through the standard `POST /api/v1/payment-method` endpoint. Use the dedicated endpoint instead:
```bash theme={null}
POST /api/v1/payment-method/virtual-account # create one
GET /api/v1/payment-method/virtual-account # list existing ones (read-only)
```
`POST` creates the account; `GET` only lists what already exists and returns `data: []` when there is none.
**Two shapes**, determined by which field you pass to `POST`:
| | Static | Dynamic |
| - | - | - |
| Pass | `label` | `amount` |
| Expires | Never | Yes, see `expiresInMinutes` in response |
| Amount-specific | No | Yes |
With a `customerId`, `NGN` and `GHS` are supported. `KES` and `USD` accounts belong to your business, so omit `customerId` to create them.
See [Create Virtual Account](/api-reference/endpoint/payment-methods/virtual-account-create) and [List Virtual Accounts](/api-reference/endpoint/payment-methods/virtual-account) for the full request and response reference.
***
## Institution fields: which one to use and when
The `institution` object has several fields that look similar. Here is exactly what each one is and when you need it.
| Field | What it is | When you need it |
| - | - | - |
| `institutionCode` | Short identifier for the bank or provider (e.g. `"058"` for GTBank Nigeria, `"MTN"` for MTN Ghana) | **Always required when creating a Payment Method** |
| `institutionName` | Human-readable display name (e.g. `"GTBank"`, `"MTN"`) | **Always required when creating a Payment Method** |
| `institutionId` | Afriex's internal database ID for the institution | **Output only. Never pass this as input.** It appears in responses but is not needed for creation. |
| `institutionAddress` | Physical address of the institution | **Only for SWIFT transfers.** Skip it for all other channels. |
### How to find the right institutionCode
1. Call `GET /api/v1/payment-method/institution?channel=BANK_ACCOUNT&countryCode=NG`
2. The response returns a list of institutions, each with `institutionCode` and `institutionName`
3. Copy those values directly into your Payment Method creation request
For SWIFT and US routing numbers, use `GET /api/v1/payment-method/institution/codes` to resolve a known SWIFT/BIC code to a bank name.
Cache the institution list per country and channel. It changes infrequently and there is no need to fetch it on every Payment Method creation.
***
## The meta object
Every transaction must include a `meta` object with two required fields. Without them, the API returns a `400` error.
```json theme={null}
"meta": {
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"reference": "ORDER-12345"
}
```
| Field | Required | What it does |
| - | - | - |
| `idempotencyKey` | **Yes** | A unique string you generate per transaction attempt. If a transaction already exists with this key, Afriex rejects the request with `409 DUPLICATE_REQUEST` rather than creating a duplicate. It does **not** replay the original transaction. Use a UUID v4. |
| `reference` | **Yes** | Your own internal ID for this transaction (e.g. order number, payment ID, or invoice reference). Used for reconciliation and returned on the transaction as `merchantReference`. |
| `narration` | No | A human-readable description (e.g. `"Salary payment for March 2025"`). |
| `invoice` | Required for SWIFT withdrawals; otherwise No | An S3 object key for the invoice document. Upload the file via [Generate Upload URL](/api-reference/endpoint/media/generate-upload-url) first, then pass the returned `key` here. The object must already exist in storage when the transaction is created. |
Never reuse an `idempotencyKey` across different transaction attempts. Use a fresh UUID for every new transaction. Reusing a key, or reusing a `reference`, is rejected with `409 DUPLICATE_REQUEST` and the new transaction is not created.
Because a duplicate is an error rather than a replay, **a `409` is not a failure to retry**. It means a transaction for that key already exists. Look it up with [List Transactions](/api-reference/endpoint/transactions/list) filtered by your `merchantReference` to find out what happened to it, instead of retrying with a fresh key and risking a genuine double payment.
***
## Common mistakes
### 1. Using a customerId as destinationId or sourceId
`destinationId` and `sourceId` must be a `paymentMethodId`, not a `customerId`. Both look like hex strings, which makes it easy to mix them up.
```json theme={null}
// Wrong: this is a customerId
"destinationId": "69516dd0464b2213bd74cfad"
// Correct: this is the paymentMethodId from Step 2
"destinationId": "690df3281c11eea59108fcaf"
```
***
### 2. Skipping the 3-step sequence
You cannot create a Transaction without first creating a Customer and a Payment Method. The API returns a `404` or `400` if either does not exist. Always follow: **Customer → Payment Method → Transaction**.
***
### 3. Mixing up DEPOSIT and WITHDRAW fields
* **WITHDRAW** uses `destinationId` (where the money goes)
* **DEPOSIT** uses `sourceId` (where the money comes from)
Passing `destinationId` on a DEPOSIT (or `sourceId` on a WITHDRAW) will cause the API to reject the request.
***
### 4. Assuming SWAP needs a payment method
SWAP does not touch any external account. It converts currency inside your Afriex business wallet. No `customerId`, `destinationId`, or `sourceId` is needed. Only the currency pair and the `meta` object are required.
```json theme={null}
{
"type": "SWAP",
"sourceAmount": "500",
"sourceCurrency": "USD",
"destinationCurrency": "NGN",
"meta": {
"idempotencyKey": "unique-uuid-here",
"reference": "SWAP-001"
}
}
```
***
### 5. Omitting meta.idempotencyKey or meta.reference
Both are required on every transaction. The API returns a `400` if either is missing. Always generate a fresh UUID for `idempotencyKey` and use your own internal order or payment ID as `reference`.
***
### 6. Hardcoding institution codes without looking them up
Do not guess institution codes. Use `GET /api/v1/payment-method/institution?channel=BANK_ACCOUNT&countryCode=NG` to get the correct code for each country and channel. The same bank can have a different code in different countries.
***
### 7. Passing institutionId as input
`institutionId` is an output field. It appears in API responses but you never need to supply it when creating a Payment Method. The fields you pass are `institutionCode` and `institutionName`.
***
## Tracking what happens after you create a transaction
Transactions do not complete instantly. After creation the status will be `PENDING` or `PROCESSING`. Here is how to know when it finishes.
**Testing in sandbox?** Sandbox transactions settle automatically in about 5 minutes. Pick the outcome with the payment method's last four digits (e.g. `0001` for fail, `0003` to hold in `PENDING` until you call [`/simulate`](/api-reference/endpoint/transactions/simulate)); add `SIMULATE_INSTANT` anywhere in `meta.reference` to speed things up to about 30 seconds. Full reference: [Testing in Sandbox](/guides/testing-in-sandbox).
**Option A (recommended): Webhooks**
Subscribe to `TRANSACTION.UPDATED` events. Afriex sends an HTTP POST to your webhook URL with the full transaction payload whenever the transaction's status changes.
This fires on intermediate statuses too, not just terminal ones, so expect several deliveries for a single transaction. Branch on the payload's `status` field to decide whether the transaction is actually finished.
See [Webhook setup](/api-reference/endpoint/webhooks/introduction) for configuration steps and payload examples.
**Option B: Poll the status endpoint**
```bash theme={null}
curl "https://sandbox.api.afriex.com/api/v1/transaction/TRANSACTION_ID" \
-H "x-api-key: YOUR_API_KEY"
```
Prefer webhooks over polling. Polling adds latency and uses API quota. Webhooks notify you the moment the status changes.
### Terminal statuses
| Status | Meaning |
| - | - |
| `SUCCESS` | Transaction finished successfully |
| `FAILED` | Transaction could not be processed. Check `meta.failureReason.code` for a stable `AFX_*` reason (see [Failure reasons](/api-reference/models#failure-reasons)). |
| `CANCELLED` | Cancelled before processing started |
| `REFUNDED` | Funds returned to the sender |
| `REJECTED` | Rejected after manual review |
***
## Next steps
Make your first API call in under 5 minutes.
Full parameter reference for customer creation.
All channels and required fields in the API reference.
Full transaction creation reference with all fields.
Complete field-by-field documentation for all models.
Receive real-time transaction status updates.
# Supported Currencies & Payment Rails
Source: https://docs.afriex.com/guides/supported-currencies
Complete list of countries, currencies, and payment channels available through the Afriex Business API
Afriex routes payments across **30+ currencies** and **100+ countries**. Use this page to confirm which payment rails are available for a given country before building your integration.
Local and international currencies
Coverage across 5 continents
Bank, mobile money, SWIFT, crypto, and more
Collections and payouts both supported
## Rail Reference
| Rail | API `channel` value | Description |
| - | - | - |
| Bank Account | `BANK_ACCOUNT` | Local bank account transfer |
| Virtual Account | `VIRTUAL_BANK_ACCOUNT` | System-generated account number for inbound deposits |
| Pool Account | `POOL_ACCOUNT` | Shared pool account for multiple users |
| Mobile Money | `MOBILE_MONEY` | Mobile money wallet (M-Pesa, MTN MoMo, Airtel, etc.) |
| SWIFT | `SWIFT` | International SWIFT wire transfer |
| Interac | `INTERAC` | Interac e-Transfer (Canada) |
| UPI | `UPI` | Unified Payments Interface (India) |
| WeChat Pay | `WE_CHAT` | WeChat Pay (China) |
| Alipay | `ALIPAY` | Alipay (China) |
| Crypto | `CRYPTO` | Crypto wallet (USDC / USDT on supported networks) |
| Paybill / Till | `PAYBILL_TILL` | M-Pesa Paybill or Till number (Kenya). Not currently creatable through the API |
***
## Supported Countries
| Country | Currency | Deposit Rails | Payout Rails |
| - | - | - | - |
| 🇧🇯 Benin | XOF | `Mobile Money` | `Mobile Money` |
| 🇧🇼 Botswana | BWP | — | `Mobile Money` |
| 🇧🇫 Burkina Faso | XOF | `Mobile Money` *(coming soon)* | `Mobile Money` *(coming soon)* |
| 🇨🇲 Cameroon | XAF | `Mobile Money` | `Bank Account` `Mobile Money` |
| 🇨🇫 Central African Republic | XAF | — | `Mobile Money` *(coming soon)* |
| 🇨🇬 Republic of Congo | XAF | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇨🇮 Côte d'Ivoire | XOF | `Mobile Money` | `Bank Account` `Mobile Money` |
| 🇨🇩 DR Congo | CDF | — | `Mobile Money` *(coming soon)* |
| 🇪🇬 Egypt | EGP | — | `Bank Account` `Mobile Money` |
| 🇪🇹 Ethiopia | ETB | `Mobile Money` | `Bank Account` `Mobile Money` |
| 🇬🇦 Gabon | XAF | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇬🇲 Gambia | GMD | — | `Mobile Money` |
| 🇬🇭 Ghana | GHS | `Mobile Money` *(coming soon)* `Virtual Account` | `Bank Account` `Mobile Money` |
| 🇬🇳 Guinea | GNF | — | `Mobile Money` |
| 🇬🇼 Guinea-Bissau | XOF | — | `Mobile Money` |
| 🇰🇪 Kenya | KES | `Mobile Money` `Virtual Account` | `Bank Account` `Mobile Money` `Paybill / Till` |
| 🇲🇬 Madagascar | MGA | — | `Mobile Money` |
| 🇲🇼 Malawi | MWK | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇲🇱 Mali | XOF | — | `Mobile Money` *(coming soon)* |
| 🇲🇦 Morocco | MAD | — | `Mobile Money` *(coming soon)* |
| 🇲🇿 Mozambique | MZN | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇳🇬 Nigeria | NGN | `Virtual Account` | `Bank Account` |
| 🇷🇼 Rwanda | RWF | `Mobile Money` *(coming soon)* | `Bank Account` `Mobile Money` |
| 🇸🇳 Senegal | XOF | — | `Bank Account` `Mobile Money` |
| 🇸🇱 Sierra Leone | SLE | `Mobile Money` *(coming soon)* | `Mobile Money` |
| 🇿🇦 South Africa | ZAR | — | `Bank Account` |
| 🇸🇸 South Sudan | SSP | — | `Mobile Money` *(coming soon)* |
| 🇹🇿 Tanzania | TZS | `Mobile Money` | `Mobile Money` |
| 🇹🇬 Togo | XOF | — | `Mobile Money` *(coming soon)* |
| 🇺🇬 Uganda | UGX | `Mobile Money` | `Bank Account` `Mobile Money` |
| 🇿🇲 Zambia | ZMW | `Mobile Money` *(coming soon)* | `Mobile Money` |
| Country | Currency | Deposit Rails | Payout Rails |
| - | - | - | - |
| 🇨🇦 Canada | CAD | `Interac` *(coming soon)* | `Interac` *(coming soon)* |
| 🇺🇸 United States | USD | `Virtual Account` | `Bank Account` `SWIFT` |
USD `SWIFT` payouts route to **100 countries** globally — not just the US. See [SWIFT Coverage](#swift-coverage) below for the full country list.
| Country | Currency | Deposit Rails | Payout Rails |
| - | - | - | - |
| 🇦🇹 Austria | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇧🇪 Belgium | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇧🇬 Bulgaria | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇭🇷 Croatia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇨🇾 Cyprus | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇨🇿 Czech Republic | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇩🇰 Denmark | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇪🇪 Estonia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇫🇮 Finland | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇫🇷 France | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇩🇪 Germany | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇬🇷 Greece | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇭🇺 Hungary | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇮🇪 Ireland | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇮🇹 Italy | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇱🇻 Latvia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇱🇹 Lithuania | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇱🇺 Luxembourg | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇲🇹 Malta | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇳🇱 Netherlands | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇳🇴 Norway | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇵🇱 Poland | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇵🇹 Portugal | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇷🇴 Romania | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇸🇰 Slovakia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇸🇮 Slovenia | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇪🇸 Spain | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇸🇪 Sweden | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇺🇦 Ukraine | EUR | `Virtual Account` *(coming soon)* | `Bank Account` |
| 🇬🇧 United Kingdom | GBP | `Virtual Account` *(coming soon)* | `Bank Account` |
EUR bank payouts use the **SEPA** network and require a valid IBAN. GBP payouts use **Faster Payments**.
| Country | Currency | Deposit Rails | Payout Rails |
| - | - | - | - |
| 🇨🇳 China | CNY | — | `Bank Account` `WeChat Pay` `Alipay` |
| 🇮🇳 India | INR | — | `Bank Account` `UPI` |
| 🇵🇰 Pakistan | PKR | — | `Bank Account` `Mobile Money` |
CNY `Bank Account` payouts require the recipient's **phone number**, **ID type**, and **ID number** in addition to standard bank details.
| Asset | Supported Countries | Deposit Rails | Payout Rails |
| - | - | - | - |
| USDC | 🌍 100+ countries globally | `Crypto` | — |
| USDT | 🌍 100+ countries globally | `Crypto` | — |
USDC and USDT deposits are accepted from 100+ countries via crypto wallet.
***
## Coming Soon
**Pool Accounts** are live. Fetch your business's shared pool account for a country with [Get Pool Account](/api-reference/endpoint/payment-methods/pool-account), then submit proof of an inbound payment with [Submit Pool Account Payment Proof](/api-reference/endpoint/transactions/pool-account-proof). Country-level coverage is still expanding.
***
## SWIFT Coverage
USD `SWIFT` payouts (`channel: SWIFT`) are available to **100 countries** — far beyond the US alone. This makes SWIFT the default choice for international USD disbursements when no local rail is available.
SWIFT transfers require: recipient email, recipient address, SWIFT/BIC code, and a transaction invoice.
| Region | Countries |
| - | - |
| **Africa** | 🇩🇿 Algeria · 🇧🇯 Benin · 🇧🇼 Botswana · 🇧🇫 Burkina Faso · 🇨🇲 Cameroon · 🇨🇫 Central African Republic · 🇨🇬 Republic of Congo · 🇨🇮 Côte d'Ivoire · 🇨🇩 DR Congo · 🇪🇬 Egypt · 🇪🇹 Ethiopia · 🇬🇦 Gabon · 🇬🇲 Gambia · 🇬🇭 Ghana · 🇬🇳 Guinea · 🇬🇼 Guinea-Bissau · 🇰🇪 Kenya · 🇲🇬 Madagascar · 🇲🇼 Malawi · 🇲🇱 Mali · 🇲🇦 Morocco · 🇲🇿 Mozambique · 🇳🇦 Namibia · 🇳🇪 Niger · 🇳🇬 Nigeria · 🇷🇼 Rwanda · 🇸🇳 Senegal · 🇸🇱 Sierra Leone · 🇿🇦 South Africa · 🇸🇸 South Sudan · 🇹🇿 Tanzania · 🇹🇬 Togo · 🇹🇳 Tunisia · 🇺🇬 Uganda · 🇿🇲 Zambia · 🇿🇼 Zimbabwe |
| **Americas** | 🇦🇷 Argentina · 🇧🇷 Brazil · 🇨🇦 Canada · 🇨🇱 Chile · 🇨🇴 Colombia · 🇭🇹 Haiti · 🇲🇽 Mexico · 🇵🇪 Peru · 🇺🇸 United States · 🇺🇾 Uruguay |
| **Europe** | 🇦🇹 Austria · 🇧🇪 Belgium · 🇧🇬 Bulgaria · 🇭🇷 Croatia · 🇨🇾 Cyprus · 🇨🇿 Czech Republic · 🇩🇰 Denmark · 🇪🇪 Estonia · 🇫🇮 Finland · 🇫🇷 France · 🇩🇪 Germany · 🇬🇮 Gibraltar · 🇬🇷 Greece · 🇭🇺 Hungary · 🇮🇪 Ireland · 🇮🇹 Italy · 🇱🇻 Latvia · 🇱🇮 Liechtenstein · 🇱🇹 Lithuania · 🇱🇺 Luxembourg · 🇲🇹 Malta · 🇳🇱 Netherlands · 🇳🇴 Norway · 🇵🇱 Poland · 🇵🇹 Portugal · 🇷🇴 Romania · 🇷🇺 Russia · 🇸🇰 Slovakia · 🇸🇮 Slovenia · 🇪🇸 Spain · 🇸🇪 Sweden · 🇨🇭 Switzerland · 🇺🇦 Ukraine · 🇬🇧 United Kingdom |
| **Asia & Pacific** | 🇦🇺 Australia · 🇧🇩 Bangladesh · 🇨🇳 China · 🇭🇰 Hong Kong · 🇮🇩 Indonesia · 🇮🇳 India · 🇯🇵 Japan · 🇰🇷 South Korea · 🇰🇼 Kuwait · 🇲🇾 Malaysia · 🇳🇿 New Zealand · 🇵🇭 Philippines · 🇵🇰 Pakistan · 🇶🇦 Qatar · 🇸🇦 Saudi Arabia · 🇸🇬 Singapore · 🇹🇭 Thailand · 🇹🇷 Turkey · 🇦🇪 UAE · 🇻🇳 Vietnam |
Use the [List Institutions](/api-reference/endpoint/payment-methods/institution) endpoint with `channel=SWIFT` to look up valid SWIFT/BIC codes for a given recipient country.
# Testing in Sandbox
Source: https://docs.afriex.com/guides/testing-in-sandbox
The complete reference for building and testing your integration against the Afriex sandbox: test values, deterministic outcomes, virtual account transfers, OTP flow, balance topups, and webhook drills.
Every Afriex environment behaves the same way on the wire: your requests, responses, webhooks and error codes are identical between sandbox and production. Sandbox just refuses to move real money.
**Base URL:** `https://sandbox.api.afriex.com`, with a sandbox API key.
Deposits, payouts, and virtual accounts can all be tested on **every payment method and currency** your business supports.
Try the sandbox demo, no API key or code required. Pick a currency and amount, click **Proceed to payment**, and walk through mobile money or bank transfer as your customers would. The demo is sandbox-only and uses the real hosted checkout page under the hood.
## When a transaction settles
A sandbox deposit or payout completes on its own about **5 minutes** after it is created, and your webhook receives the final status exactly as in production. When you need it sooner:
* Include `SIMULATE_INSTANT` anywhere in `meta.reference` when you create the transaction, and it completes in about **30 seconds**.
* Or call [Simulate Transaction Outcome](/api-reference/endpoint/transactions/simulate) with `{"outcome": "success"}` or `{"outcome": "failed"}` to finalize it immediately.
Either way the `TRANSACTION.UPDATED` webhook is sent when the transaction settles.
## Choosing the outcome with the last 4 digits
The last four digits of the **account number**, the **phone number**, or the **card number** decide what happens. Everything else about the account is up to you, so you can build one payment method per outcome and reuse it across tests. A payment method with none of these to key off (an email or a wallet address, for example) simply succeeds unless you complete it with [`/simulate`](#simulating-a-pending-transaction).
| Ends in | Payout (`WITHDRAW`) | Mobile money deposit (`DEPOSIT`) | Card deposit (`DEPOSIT`, via [checkout](/api-reference/endpoint/checkout-sessions/create)) |
| - | - | - | - |
| anything else | Succeeds | Succeeds | Succeeds |
| `0001` | Fails; your balance is refunded | Fails | Fails |
| `0002` | Succeeds | Waits for a one-time password (see [OTP](#otp-required-mobile-money-deposits)) | Succeeds |
| `0003` | Stays pending until you call [`/simulate`](#simulating-a-pending-transaction) | Stays pending until you call [`/simulate`](#simulating-a-pending-transaction) | Stays pending until you call [`/simulate`](#simulating-a-pending-transaction) |
| `0004` | Account name lookup ([`/resolve`](/api-reference/endpoint/payment-methods/resolve)) fails | Succeeds | Succeeds |
| `0005` | Declined immediately | Declined immediately | Declined immediately |
**Cards are hosted-checkout only.** The "Card deposit" column above applies to card payments made through a [checkout session](/api-reference/endpoint/checkout-sessions/create); [Create Payment Method](/api-reference/endpoint/payment-methods/create) does not accept `CARD` as a channel.
A phone number must still be a valid mobile number for its country, so keep the country prefix and change only the last digits. For example `+254712340002` in Kenya. Account name lookups against a sandbox account return **`Sandbox Test Account`**.
## Simulating a pending transaction
Any payment method ending in `0003` puts the transaction in `PENDING` and holds it there until you tell the sandbox what to do. Call [Simulate Transaction Outcome](/api-reference/endpoint/transactions/simulate):
```bash theme={null}
curl -X POST \
-H "x-api-key: YOUR_SANDBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"outcome": "success"}' \
https://sandbox.api.afriex.com/api/v1/transaction/TRANSACTION_ID/simulate
```
Pass `{"outcome": "failed"}` to test your failure path. The result arrives by the `TRANSACTION.UPDATED` webhook shortly after this call returns.
You can use the same endpoint to complete **any** pending sandbox transaction early, not just ones ending in `0003`. The transaction must still be `PENDING`, `PROCESSING`, or `UNKNOWN`; a deposit waiting on OTP (`CUSTOMER_ACTION_REQUIRED`) has to be authorized first.
## OTP-required mobile money deposits
A mobile-money deposit from a wallet ending in `0002` returns `CUSTOMER_ACTION_REQUIRED`. Complete it with [Authorize Transaction](/api-reference/endpoint/transactions/authorize) using the sandbox OTP **`123456`**:
```bash theme={null}
curl -X POST \
-H "x-api-key: YOUR_SANDBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"OTP","otp":"123456"}' \
https://sandbox.api.afriex.com/api/v1/transaction/TRANSACTION_ID/authorize
```
Any other OTP returns `OTP_INCORRECT`, so you can test the wrong-OTP path too. After authorization the deposit completes on the normal sandbox timeline (or you can accelerate it via [`/simulate`](#simulating-a-pending-transaction)).
## Simulating an incoming bank transfer
Sandbox virtual accounts receive no money on their own. Pay into one with [Simulate Virtual Account Transfer](/api-reference/endpoint/payment-methods/virtual-account-simulate-transfer):
```bash theme={null}
# Reusable virtual account (created without an amount)
curl -X POST \
-H "x-api-key: YOUR_SANDBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"accountNumber": "4821930275", "amount": 5000, "currency": "NGN"}' \
https://sandbox.api.afriex.com/api/v1/payment-method/virtual-account/simulate-transfer
```
For a **one-time** virtual account (created with an `amount`), also pass the `reference` the account was issued with. Pass `"outcome": "failed"` to simulate a failed inflow. The credit lands as a transaction and is reported by webhook.
## Testing cards
Cards are only collected through a hosted [checkout session](/api-reference/endpoint/checkout-sessions/create), never through [Create Payment Method](/api-reference/endpoint/payment-methods/create). To test a card deposit:
Call [Create Checkout Session](/api-reference/endpoint/checkout-sessions/create) with `channels` including `"CARD"`. Redirect the payer to the returned `checkoutUrl`.
On the hosted page, use a test card whose **last four digits** pick the outcome from the [table above](#choosing-the-outcome-with-the-last-4-digits) (`0001` fails, `0003` stays pending, and so on). The hosted page pre-fills a valid test card in read-only form on sandbox, so you can click **Pay** without typing anything.
The card deposit lands as a `DEPOSIT` transaction and settles on the sandbox timeline described above. Use [`/simulate`](#simulating-a-pending-transaction) to finalize a `0003` pending card deposit on demand.
## Hosted checkout in sandbox
A checkout session started with a sandbox API key opens the hosted page in **test mode**, with helpers built in so you don't have to memorize the test values above:
* A **Test mode** badge in the page header.
* The mobile-money **test numbers** (`0001` fails, `0002` asks for an OTP, `0005` is declined) are listed on the mobile-money screen.
* The **OTP hint** (`123456`) is shown when an OTP is required.
* **Simulate transfer** / **Simulate failed transfer** buttons on the bank-transfer screen finalize the deposit without you needing to leave the page.
* Card fields are pre-filled with a valid **test card** in read-only form.
Want to try the hosted checkout end to end without touching the API first? Open the [sandbox demo](https://sandbox.pay.afriex.com/demo), pick a currency and amount, and click **Proceed to payment**. All three channels are covered, allocated per currency: **mobile money** (KES, GHS and others), **bank transfer** (NGN), and **card** (USD). Use the same test values documented above to drive different outcomes.
## Funding your sandbox balance
Payouts and swaps need funds in your sandbox wallets. Call [Top Up Sandbox Balance](/api-reference/endpoint/balance/topup) whenever you need more:
```bash theme={null}
curl -X POST \
-H "x-api-key: YOUR_SANDBOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"currency": "USD", "amount": 10000}' \
https://sandbox.api.afriex.com/api/v1/org/balance/topup
```
The topup endpoint only works in sandbox; production returns `403`.
## Drilling your webhook handler
Verify signatures, retries, and downstream handling with a synthetic event from [Trigger Test Webhook](/api-reference/endpoint/webhooks/trigger). Configure your webhook URL in the [Dashboard](https://business.afriex.com/) first.
## Checklist before going live
Once you're comfortable in sandbox:
Run a `success`, a `failed`, an OTP flow (`0002`), and a `0003` pending flow. Confirm your webhook handler branches correctly on each.
Trigger a test webhook and confirm you reject a tampered body and accept a valid one.
Look up transactions by `merchantReference` and confirm your idempotency keys behave as expected on retry.
Swap `sandbox.api.afriex.com` for `api.afriex.com` and your sandbox key for a production key. No other code changes are required.
Sandbox and production run the same code paths. If you see divergence, email `support@afriex.com` with the request id and we'll dig in.
# Afriex Business API
Source: https://docs.afriex.com/index
Build seamless payment experiences with the Afriex Business API
## Welcome
The Afriex Business API enables businesses to manage customers, process cross-border payments, and handle payouts across Africa and beyond. Integrate our API to send and receive payments in multiple currencies.
Get up and running with your first API call in minutes.
Prefer a step-by-step walkthrough?\
Read the blog: [https://dev.to/afriex/getting-started-with-the-afriex-api-from-signup-to-your-first-request-bjk](https://dev.to/afriex/getting-started-with-the-afriex-api-from-signup-to-your-first-request-bjk)
## Core Features
Create and manage customer profiles with KYC verification.
Set up bank accounts, mobile money, and crypto wallets.
Process deposits and withdraws with real-time tracking.
Access real-time exchange rates for all supported currencies.
## Supported Markets
Afriex supports payments across a wide range of African countries and international markets:
Nigeria (NG), Ghana (GH), Côte d'Ivoire (CI), Senegal (SN), Benin (BJ), Guinea (GN), Sierra Leone (SL), Gambia (GM), Guinea-Bissau (GW)
Kenya (KE), Uganda (UG), Tanzania (TZ), Rwanda (RW), Ethiopia (ET)
Cameroon (CM), Congo Brazzaville (CG)
South Africa (ZA), Zambia (ZM), Malawi (MW), Mozambique (MZ), Madagascar (MG), Botswana (BW)
Egypt (EG)
India, China, Pakistan
United States (US), United Kingdom (GB), Canada (CA), EEA Countries
Supported countries vary by payment channel (bank transfer, mobile money, SWIFT, etc.). Use the [List Institutions](/api-reference/endpoint/payment-methods/institution) endpoint to check available channels and providers for a specific country.
## Get Started
Explore the complete API documentation.
Make your first API call in 5 minutes.
Set up real-time event notifications.
Contact our team for help.
**Need help?** Email us at [support@afriex.com](mailto:support@afriex.com).
# Connecting MCP Clients
Source: https://docs.afriex.com/mcp/connecting
Connect Claude Desktop, Claude Code, and Cursor to the Afriex MCP server
Connect your MCP client to the Afriex MCP server at `https://mcp.afriex.com/mcp`.
There are two ways to provide your API key:
* **HTTP headers**: pass your API key directly in the client config. The server stores these values for the session and uses them for all Business API calls. Recommended when your client supports custom headers.
* **`authenticate` tool**: call the `authenticate` tool as your first action after connecting. Works with all clients.
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
**With HTTP headers (recommended):**
```json theme={null}
{
"mcpServers": {
"afriex": {
"url": "https://mcp.afriex.com/mcp",
"headers": {
"x-afriex-api-key": "your-api-key",
"x-afriex-environment": "production"
}
}
}
}
```
**With `authenticate` tool:**
```json theme={null}
{
"mcpServers": {
"afriex": {
"url": "https://mcp.afriex.com/mcp"
}
}
}
```
Restart Claude Desktop. The Afriex tools will appear in the tools menu. Then call the `authenticate` tool:
```json theme={null}
{
"apiKey": "your-api-key",
"environment": "production"
}
```
**With HTTP headers (recommended):**
Add to `.claude/settings.json`:
```json theme={null}
{
"mcpServers": {
"afriex": {
"url": "https://mcp.afriex.com/mcp",
"headers": {
"x-afriex-api-key": "your-api-key",
"x-afriex-environment": "production"
}
}
}
}
```
**With `authenticate` tool:**
```bash theme={null}
claude mcp add afriex --transport http https://mcp.afriex.com/mcp
```
Or add manually to `.claude/settings.json`:
```json theme={null}
{
"mcpServers": {
"afriex": {
"url": "https://mcp.afriex.com/mcp"
}
}
}
```
Then call the `authenticate` tool:
```json theme={null}
{
"apiKey": "your-api-key",
"environment": "production"
}
```
**With HTTP headers (recommended):**
Add to `.cursor/mcp.json` in your project root:
```json theme={null}
{
"mcpServers": {
"afriex": {
"url": "https://mcp.afriex.com/mcp",
"headers": {
"x-afriex-api-key": "your-api-key",
"x-afriex-environment": "production"
}
}
}
}
```
**With `authenticate` tool:**
Go to **Cursor Settings → MCP → Add new MCP Server**, select **Type: URL**, and enter `https://mcp.afriex.com/mcp`.
Or add manually to `.cursor/mcp.json`:
```json theme={null}
{
"mcpServers": {
"afriex": {
"url": "https://mcp.afriex.com/mcp"
}
}
}
```
Then call the `authenticate` tool:
```json theme={null}
{
"apiKey": "your-api-key",
"environment": "production"
}
```
# MCP Server
Source: https://docs.afriex.com/mcp/introduction
Use the Afriex Business API with AI assistants via the Model Context Protocol
The Afriex MCP server exposes the Afriex Business API as tools for AI assistants. It implements the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), letting clients like Claude Desktop, Claude Code, and Cursor interact with the Afriex platform programmatically.
## How it works
Each Afriex Business API endpoint becomes an individually callable MCP tool. An AI assistant can create customers, send transactions, look up balances, and more, all via natural language.
## Authentication
You will need an Afriex Business API key from the [dashboard](https://business.afriex.com/). Connect your MCP client to `https://mcp.afriex.com/mcp`, then authenticate using one of two methods:
**Method 1: HTTP headers (recommended)**
Pass your API key directly in your MCP client config. The server captures these values and uses them for all subsequent Business API calls in the session:
| Header | Required | Description |
| - | - | - |
| `x-afriex-api-key` | Yes | Your Afriex Business API key |
| `x-afriex-environment` | No | `production` or `development` (defaults to `development`) |
**Method 2: `authenticate` tool**
After connecting, call the `authenticate` tool as your first action:
```json theme={null}
{
"apiKey": "your-api-key",
"environment": "production"
}
```
See [Connecting MCP Clients](/mcp/connecting) for client-specific setup instructions.
## Available Tools (26)
### Authentication
| Tool | Description |
| - | - |
| `authenticate` | Set your API key and environment (`production`/`development`) for this session |
| `session_info` | View current session config: masked API key, environment, and base URL |
### Customers
| Tool | Description |
| - | - |
| `create_customer` | Create a new customer with name, email, phone, and country code |
| `list_customers` | Retrieve a paginated list of customers |
| `get_customer` | Get a customer by ID |
| `delete_customer` | Delete a customer by ID |
| `update_customer_kyc` | Update KYC information for a customer |
### Transactions
| Tool | Description |
| - | - |
| `create_transaction` | Create a `DEPOSIT`, `WITHDRAW`, or `SWAP` transaction |
| `list_transactions` | Retrieve a paginated list of transactions |
| `get_transaction` | Get a transaction by ID |
### Payment Methods
| Tool | Description |
| - | - |
| `create_payment_method` | Create a payment method (`BANK_ACCOUNT`, `SWIFT`, `MOBILE_MONEY`, `UPI`, `INTERAC`, `WE_CHAT`) |
| `list_payment_methods` | Retrieve a paginated list of payment methods |
| `get_payment_method` | Get a payment method by ID |
| `delete_payment_method` | Delete a payment method by ID |
| `list_institutions` | List banks/providers by country and channel |
| `resolve_institution_codes` | Resolve a SWIFT code or routing number to a bank name |
| `resolve_payment_method` | Resolve recipient info by account number or phone |
| `get_crypto_wallet` | Get or create a crypto wallet (USDT/USDC, production only) |
| `list_virtual_accounts` | List existing virtual accounts for a customer and currency (production only) |
| `create_virtual_account` | Create a virtual account, static (`label`) or dynamic (`amount`) (production only) |
| `get_pool_account` | Get the business's shared pool account for a country |
### Checkout Sessions
| Tool | Description |
| - | - |
| `create_checkout_session` | Create a hosted checkout session and return its `checkoutUrl` |
### Webhooks
| Tool | Description |
| - | - |
| `trigger_webhook` | Fire a real signed test webhook to your configured callback URL (sandbox/staging only) |
### Organization
| Tool | Description |
| - | - |
| `get_balance` | Fetch wallet balances for specified currencies |
| `get_rates` | Get real-time exchange rates |
| `topup_balance` | Top up sandbox balance with a specified amount and currency (sandbox/staging only) |
# Quickstart
Source: https://docs.afriex.com/quickstart
Get up and running with the Afriex Business API in minutes
## Quickstart Guide
Get up and running with the Afriex Business API in three simple steps.
Want to see what the hosted checkout looks like before writing any code? Try the [sandbox demo](https://sandbox.pay.afriex.com/demo). No API key required.
## Prerequisites
Before you begin, you'll need:
* An Afriex Business account ([Sign up here](https://business.afriex.com/))
* Your API key from the dashboard
* A tool for making HTTP requests (cURL, Postman, or your preferred HTTP client)
## Step 1: Get your API key
1. Log in to the [Afriex Dashboard](https://business.afriex.com/)
2. Navigate to **Settings** > **API Keys**
3. Generate a new API key for your integration
4. Copy and securely store your API key
The dashboard has a **Test / Live toggle** at the top. Make sure you are in **Test** mode while developing so you use sandbox credentials. Switch to **Live** mode only when you are ready to go to production. Each mode has different API keys.
Keep your API key secure and never expose it in client-side code or public repositories.
## Step 2: Make your first API call
Create your first customer:
```bash theme={null}
curl -X POST https://sandbox.api.afriex.com/api/v1/customer \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"fullName": "John Doe",
"email": "john.doe@example.com",
"phone": "+2348192837465",
"countryCode": "NG"
}'
```
Use the staging environment for testing. Switch to `https://api.afriex.com` for production.
A successful response will return the created customer:
```json theme={null}
{
"data": {
"customerId": "69516dd0464b2213bd74cfad",
"name": "John Doe",
"email": "john.doe@example.com",
"phone": "+2348192837465",
"countryCode": "NG"
}
}
```
## Step 3: Process your first transaction
Once you have a customer and payment method set up, you can create a transaction:
```bash theme={null}
curl -X POST https://sandbox.api.afriex.com/api/v1/transaction \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"customerId": "CUSTOMER_ID",
"type": "WITHDRAW",
"sourceAmount": "10",
"destinationAmount": 5000,
"sourceCurrency": "USD",
"destinationCurrency": "NGN",
"destinationId": "PAYMENT_METHOD_ID",
"meta": {
"reference": "order-12345",
"idempotencyKey": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
}'
```
In **sandbox**, this transaction settles automatically in about 5 minutes so you can test the full flow. To pick the outcome, use a payment method whose last four digits are `0001` (fails) or `0003` (stays pending until you call [`/simulate`](/api-reference/endpoint/transactions/simulate)). To skip the wait, add `SIMULATE_INSTANT` anywhere in `meta.reference` and it settles in about 30 seconds. See the [Testing in Sandbox guide](/guides/testing-in-sandbox) for the full test-value taxonomy.
## Next Steps
Now that you've made your first API calls, explore these guides:
**Planning to use webhooks?** Allowlist these Afriex IP addresses on your server so webhook deliveries are not blocked:
* Sandbox: `34.234.189.210`
* Production: `34.197.33.100`
See [Webhook setup](/api-reference/endpoint/webhooks/introduction) for full details.
Explore all available endpoints and their parameters.
Learn how to manage customer profiles and KYC.
Set up bank accounts and mobile money wallets.
Receive real-time notifications for events.
## Support
Need help? Contact us at [support@afriex.com](mailto:support@afriex.com).
# Afriex SDK
Source: https://docs.afriex.com/sdk/introduction
TypeScript SDK for the Afriex Business API: type-safe, modular, and production-ready
This SDK was built by the **Afriex DevRel team**, winners of our hackathon. We loved what they built so much that it became the official SDK. Big shoutout to the team for making this happen.
## Overview
The Afriex SDK is a TypeScript library that wraps the Afriex Business API with full type definitions, built-in retry logic, and modular imports. Most REST endpoints in the [API reference](/api-reference/introduction) have a matching SDK method, and where one exists the TypeScript snippet appears as a tab in the right-hand code panel on that endpoint's page. A few endpoints are not covered by the SDK yet, including SME registration, presigned media uploads, settlement advice and pool-account payment proof; call those over REST.
This page covers what is SDK-specific: install, init, configuration, retries, environments, and webhook signature handling.
## Installation
```bash theme={null}
npm install @afriex/sdk
# or
pnpm add @afriex/sdk
# or
yarn add @afriex/sdk
```
## Quick Start
### 1. Initialize the SDK
```typescript theme={null}
import { AfriexSDK } from "@afriex/sdk";
const afriex = new AfriexSDK({
apiKey: process.env.AFRIEX_API_KEY,
environment: "staging", // or 'production'
});
```
### 2. Make your first calls
```typescript theme={null}
const customer = await afriex.customers.create({
fullName: "John Doe",
email: "john@example.com",
phone: "+1234567890",
countryCode: "US",
});
const rates = await afriex.rates.getRates({
fromSymbols: "USD",
toSymbols: "NGN,GBP",
});
const balances = await afriex.balance.getBalance({
currencies: ["USD", "NGN"],
});
```
For the full set of methods, browse the [API reference](/api-reference/introduction); each endpoint page shows the equivalent SDK call.
## Configuration
| Option | Type | Default | Description |
| :- | :- | :- | :- |
| `apiKey` | `string` | Required | Your Afriex API key |
| `environment` | `string` | `'production'` | `'staging'` or `'production'` |
| `webhookPublicKey` | `string` | (none) | Public key for webhook verification |
| `retryConfig` | `RetryConfig` | `{ maxRetries: 0 }` | Request retry configuration |
### Enable Retries
Retries are disabled by default. Enable them with `retryConfig`:
```typescript theme={null}
const afriex = new AfriexSDK({
apiKey: process.env.AFRIEX_API_KEY,
retryConfig: {
maxRetries: 3,
retryDelay: 1000,
retryableStatusCodes: [408, 429, 500, 502, 503, 504],
},
});
```
## Environments
| Environment | Base URL |
| :- | :- |
| Staging | `https://sandbox.api.afriex.com` |
| Production | `https://api.afriex.com` |
Get your API key from the [Afriex Dashboard](https://business.afriex.com) under the **Developer** tab.
## Webhooks
The SDK ships helpers for verifying webhook signatures and parsing event payloads. See [Webhooks](/sdk/webhooks).
# Webhooks
Source: https://docs.afriex.com/sdk/webhooks
Verify webhook signatures and handle event notifications with the SDK
The SDK ships helpers for verifying webhook signatures and parsing event payloads. For the full list of webhook events and payload shapes, see the [Webhooks reference](/api-reference/endpoint/webhooks/introduction).
## Setup
Pass your webhook public key when initializing the SDK:
```typescript theme={null}
const afriex = new AfriexSDK({
apiKey: "your-api-key",
webhookPublicKey: "your-afriex-public-key",
});
```
## Verify and Parse
Validate the signature and parse the payload in one call:
```typescript theme={null}
app.post("/webhook", (req, res) => {
const signature = req.headers["x-webhook-signature"];
const payload = req.body; // Raw string body
try {
const event = afriex.webhooks.verifyAndParse(payload, signature);
switch (event.event) {
case "CUSTOMER.CREATED":
case "CUSTOMER.UPDATED":
case "CUSTOMER.DELETED":
console.log("Customer event:", event.data);
break;
case "TRANSACTION.CREATED":
case "TRANSACTION.UPDATED":
console.log("Transaction event:", event.data);
break;
case "PAYMENT_METHOD.CREATED":
case "PAYMENT_METHOD.UPDATED":
case "PAYMENT_METHOD.DELETED":
console.log("Payment method event:", event.data);
break;
case "CHECKOUT_SESSION.CREATED":
console.log("Checkout session event:", event.data);
break;
case "POOL_DEPOSIT_REQUEST.REJECTED":
console.log("Pool deposit rejected:", event.data);
break;
}
res.status(200).send("OK");
} catch (error) {
console.error("Invalid signature");
res.status(400).send("Invalid signature");
}
});
```
## Verify Only
Validate the signature without parsing:
```typescript theme={null}
const isValid = afriex.webhooks.verify(payload, signature);
if (isValid) {
const event = JSON.parse(payload);
// Handle event
}
```
## Trigger Test Webhook (Sandbox)
Send a test webhook to your endpoint in sandbox:
```typescript theme={null}
const result = await afriex.webhooks.triggerTestWebhook({
event: "TRANSACTION.UPDATED",
resourceId: "transaction-id",
});
console.log(result); // { success: true, message: "..." }
```
For `CHECKOUT_SESSION.CREATED`, use the checkout session UUID as `resourceId`.
Test webhook triggers only work in the sandbox environment.