curl --request POST \
--url https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"outcome": "success"
}
'import requests
url = "https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate"
payload = { "outcome": "success" }
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({outcome: 'success'})
};
fetch('https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'outcome' => 'success'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate"
payload := strings.NewReader("{\n \"outcome\": \"success\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"outcome\": \"success\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"outcome\": \"success\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"transactionId": "<string>",
"customerId": "<string>",
"sourceId": "<string>",
"destinationId": "<string>",
"sourceAmount": "<string>",
"sourceCurrency": "<string>",
"destinationAmount": "<string>",
"destinationCurrency": "<string>",
"type": "DEPOSIT",
"channel": "BANK_ACCOUNT",
"status": "PENDING",
"merchantReference": "<string>",
"rate": "<string>",
"fee": "<string>",
"meta": {
"narration": "<string>",
"otpRequired": true,
"failureReason": {
"code": "AFX_VELOCITY_LIMIT_EXCEEDED",
"message": "<string>",
"retryable": true
}
},
"createdAt": "<string>",
"updatedAt": "<string>"
}
}{
"code": "<string>",
"error": "<string>",
"details": {
"errorMessage": "<string>",
"friendlyMessage": "<string>",
"data": {
"customerId": "<string>"
}
}
}{
"code": "<string>",
"error": "<string>",
"details": {
"errorMessage": "<string>",
"friendlyMessage": "<string>",
"data": {
"customerId": "<string>"
}
}
}Simulate Transaction Outcome
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.
curl --request POST \
--url https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate \
--header 'Content-Type: application/json' \
--header 'x-api-key: <api-key>' \
--data '
{
"outcome": "success"
}
'import requests
url = "https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate"
payload = { "outcome": "success" }
headers = {
"x-api-key": "<api-key>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'x-api-key': '<api-key>', 'Content-Type': 'application/json'},
body: JSON.stringify({outcome: 'success'})
};
fetch('https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'outcome' => 'success'
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate"
payload := strings.NewReader("{\n \"outcome\": \"success\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("x-api-key", "<api-key>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate")
.header("x-api-key", "<api-key>")
.header("Content-Type", "application/json")
.body("{\n \"outcome\": \"success\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://sandbox.api.afriex.com/api/v1/transaction/{transactionId}/simulate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["x-api-key"] = '<api-key>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"outcome\": \"success\"\n}"
response = http.request(request)
puts response.read_body{
"data": {
"transactionId": "<string>",
"customerId": "<string>",
"sourceId": "<string>",
"destinationId": "<string>",
"sourceAmount": "<string>",
"sourceCurrency": "<string>",
"destinationAmount": "<string>",
"destinationCurrency": "<string>",
"type": "DEPOSIT",
"channel": "BANK_ACCOUNT",
"status": "PENDING",
"merchantReference": "<string>",
"rate": "<string>",
"fee": "<string>",
"meta": {
"narration": "<string>",
"otpRequired": true,
"failureReason": {
"code": "AFX_VELOCITY_LIMIT_EXCEEDED",
"message": "<string>",
"retryable": true
}
},
"createdAt": "<string>",
"updatedAt": "<string>"
}
}{
"code": "<string>",
"error": "<string>",
"details": {
"errorMessage": "<string>",
"friendlyMessage": "<string>",
"data": {
"customerId": "<string>"
}
}
}{
"code": "<string>",
"error": "<string>",
"details": {
"errorMessage": "<string>",
"friendlyMessage": "<string>",
"data": {
"customerId": "<string>"
}
}
}403 in production.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
0003staysPENDINGuntil you call this endpoint. - Speed up any other pending sandbox transaction. Call it to finalize sooner than the ~5 minute default.
PENDING, PROCESSING or UNKNOWN. A deposit waiting on OTP (CUSTOMER_ACTION_REQUIRED) must be authorized via Authorize Transaction first.
See the Sandbox testing guide for the full test-value taxonomy.Authorizations
Static business API key issued from the dashboard. A business can
provision multiple API keys, each scoped to a configurable set of
permissions (e.g. read transactions, create deposits, etc). Permissions are chosen per key at creation time in the
dashboard and may be revoked by deleting the key. Requests made with a
key that does not include the permission required by the target
endpoint is rejected with a 401 Unauthorized response, the same
response an unrecognised, malformed, revoked or disabled key returns. The API
does not distinguish the two cases on the wire.
Manage your keys and their permissions under Developer → API keys
in the dashboard.
The permission each endpoint requires is below. Where several are listed, any one of them is enough. Keys created before permissions existed carry none and keep access to every endpoint.
| 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 |
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 |
Headers
API version in ISO 8601 format. The only supported version is 2026-05-18, which is also the default when the header is omitted. Any other value is rejected with a 400 Bad Request.
Path Parameters
The unique identifier of the transaction
Body
The status to finalize the transaction to.
success, failed Response
Accepted. The transaction as it stands before it is finalized.
Show child attributes
Show child attributes
