CAPSEND MTO API
The CAPSEND MTO API lets partners create payout transactions and retrieve transaction status from their backend systems.
Environments
Set your integration base URL as an environment variable and use it in every request.
| Environment | BASE_URL | Use for |
|---|---|---|
| Staging | https://staging.capsendimto.org/v1 |
Testing API credentials, request formatting, webhook handling, and transaction status flows. |
| Production | https://prod.capsendimto.org/v1 |
Live integrations and real transactions. |
# Staging
BASE_URL="https://staging.capsendimto.org/v1"
# Production
BASE_URL="https://prod.capsendimto.org/v1"
API Client Setup
Create API clients from the CAPSEND MTO platform before calling the API. API clients are environment-scoped:
- Create a staging API client first and use it with the staging
BASE_URLwhile building and testing. - Configure the webhook URL that should receive staging events.
- Store the generated
clientIdandclientSecretsecurely. The secret should only be shown once. - When your integration is ready for live processing, create a separate production API client on the MTO platform.
- Use production credentials only with the production
BASE_URL. Production calls create real transactions.
Request Basics
Content type
Send request bodies as JSON.
Content-Type: application/json
Authentication
Authenticated endpoints require a bearer token from the Token endpoint.
Authorization: Bearer ACCESS_TOKEN
Status Codes
| Code | Meaning |
|---|---|
200 |
Request completed successfully |
400 |
Invalid request payload or missing required field |
401 |
Missing, invalid, or expired token |
404 |
Resource not found |
409 |
Duplicate or conflicting resource |
500 |
Internal server error |
Error Response
{
"message": "Missing or invalid token",
"code": "UNAUTHORIZED",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
Token
Generate an access token using the API client credentials for the environment you are calling.
POST /api/token
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
clientId |
string | Yes | API client ID issued by CAPSEND. |
clientSecret |
string | Yes | API client secret issued by CAPSEND. |
Sample Request
curl -X POST "${BASE_URL}/api/token" \
-H "Content-Type: application/json" \
-d '{
"clientId": "capcid_1234567890abcdef",
"clientSecret": "your_client_secret"
}'
Sample Response
{
"accessToken": "eyJhbGciOi...",
"tokenType": "Bearer",
"expiresIn": 86400
}
Create Transaction
Create a payout transaction. CAPSEND validates the MTO wallet balance, places a hold, and processes the transaction asynchronously.
POST /transactions/createTransaction
Staging Test Rules
When using the staging BASE_URL, transaction requests are for testing only:
| Rule | Value |
|---|---|
| Fixed transaction amount | 100 |
| Non-Zenith test bank | Sudo Bank |
| Non-Zenith test bank code | 999998 |
| Non-Zenith test account number | 0000014575 |
Use the Sudo Bank details only when testing transfers where the beneficiary bank is not Zenith Bank. Production transactions should use real beneficiary details approved for your integration.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer ACCESS_TOKEN |
Content-Type |
string | Yes | application/json |
idempotency-key |
uuid | Yes | Unique UUID for safely retrying the same request. |
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
amount |
number | Yes | Transaction amount. In staging, this must be 100. |
currency |
string | Yes | 3-letter wallet currency.USD, GBP, EUR. |
transactionType |
string | Yes | PAYOUT. |
crAccount |
string | Yes | Destination account number. For staging non-Zenith tests, use 0000014575. |
narration |
string | Yes | Transfer narration. |
country |
string | Yes | Sender country. |
senderAddress |
string | Yes | Sender address. |
senderName |
string | Yes | Sender full name. |
beneficiaryBankName |
string | Yes | Destination bank name. |
beneficiaryName |
string | Yes | Beneficiary full name. |
beneficiaryAddress |
string | Yes | Beneficiary address. |
stateCode |
string | Yes | Sender state code. |
description |
string | No | Transfer description. |
beneficiaryBankCode |
string | Conditional | Required for non-Zenith bank transfers. For staging non-Zenith tests, use 999998. |
externalReference |
string | No | Partner reference. |
Sample Request
curl -X POST "${BASE_URL}/transactions/createTransaction" \
-H "Authorization: Bearer ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "idempotency-key: 550e8400-e29b-41d4-a716-446655440000" \
-d '{
"amount": 100,
"currency": "USD",
"transactionType": "PAYOUT",
"crAccount": "0000014575",
"narration": "Staging test payout for INV-1001",
"country": "NGN",
"senderAddress": "100 Market Street, San Francisco",
"senderName": "Jane Sender",
"beneficiaryBankName": "Sudo Bank",
"beneficiaryBankCode": "999998",
"beneficiaryName": "John Beneficiary",
"beneficiaryAddress": "12 Marina Road, Lagos",
"stateCode": "LA",
"externalReference": "INV-1001"
}'
Sample Response
{
"transactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"reference": "CAP17140590000001",
"status": "PENDING"
}
Transaction Status Values
| Status | Description |
|---|---|
PENDING |
Transaction has been created and queued for processing. |
PROCESSING |
Transaction is being processed. |
PROCESSED |
Transaction has been processed. |
SUCCESS |
Transaction completed successfully. |
FAILED |
Transaction failed. |
REFUNDED |
Transaction has been refunded. |
Fetch Transaction
Retrieve one transaction by CAPSEND transaction ID.
GET /transactions/get/:id
This is the public API route for partner integrations. It uses the API client bearer token returned by POST /api/token. Dashboard transaction routes use user authentication and should not be used for server-to-server API integrations.
Headers
| Header | Type | Required | Description |
|---|---|---|---|
Authorization |
string | Yes | Bearer ACCESS_TOKEN |
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | CAPSEND transaction ID returned from Create Transaction. |
Sample Request
curl "${BASE_URL}/transactions/get/7c9e6679-7425-40de-944b-e07fc1f90ae7" \
-H "Authorization: Bearer ACCESS_TOKEN"
Sample Response
{
"id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"reference": "CAP17140590000001",
"status": "SUCCESS",
"createdAt": "2026-04-25T10:00:00.000Z",
"failureReason": null,
"processedAt": "2026-04-25T10:00:05.000Z",
"succeededAt": "2026-04-25T10:00:08.000Z",
"failedAt": null
}
Webhooks
CAPSEND sends webhook events to the webhook URL configured for your API client environment. Every webhook request is signed so your server can verify that the payload came from CAPSEND and was not changed in transit.
Webhook Headers
| Header | Description |
|---|---|
x-webhook-id |
CAPSEND webhook configuration ID that sent the event. |
x-webhook-signature |
HMAC SHA-256 signature in the format sha256=<hex>. |
x-webhook-timestamp |
Unix timestamp in seconds. Reject requests older than 5 minutes. |
Event Payload
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "transaction.processed",
"createdAt": "2026-04-25T10:00:08.000Z",
"data": {
"transactionId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"reference": "CAP17140590000001",
"amount": 100,
"currency": "USD"
}
}
Events
| Event | Description |
|---|---|
transaction.processed |
Transaction completed processing successfully. |
transaction.failed |
Transaction failed. |
Verification
Use this process in any programming language:
- Read and preserve the raw request body exactly as received, before parsing JSON or formatting it again.
- Read
x-webhook-signatureandx-webhook-timestampfrom the request headers. - Parse
x-webhook-timestampas Unix seconds and reject the request if it is more than 300 seconds from your server time. - Compute an HMAC SHA-256 digest using your webhook secret as the key and the raw request body as the message.
- Hex-encode the digest in lowercase and prefix it with
sha256=. - Compare that expected value with
x-webhook-signatureusing a constant-time comparison. - Parse and process the JSON event only after verification succeeds.
Language-neutral pseudocode:
raw_body = request.raw_body
received_signature = request.header["x-webhook-signature"]
timestamp = request.header["x-webhook-timestamp"]
if abs(current_unix_seconds() - parse_integer(timestamp)) > 300:
reject_request()
digest = hmac_sha256_hex(secret = webhook_secret, message = raw_body)
expected_signature = "sha256=" + digest
if not constant_time_equal(received_signature, expected_signature):
reject_request()
event = parse_json(raw_body)
process_event_once(event.id)
return 200
Respond with 200 OK after receiving the event.