Tilt Pay API · Client Pack (IMT)
Send transfers and collect payments across Africa through the Tilt platform. Instant. Secure. Scalable.
Introduction
The Tilt Pay API is based on REST with resource-oriented URLs. It accepts JSON-encoded request bodies and returns JSON-encoded responses with standard HTTP response codes. Learn more about the Tilt platform at tiltafrica.com.
This client pack covers the International Money Transfer (IMT) flow: authenticating, discovering institutions, checking FX rates, looking up recipients, and creating, quoting and tracking transfers. Sending within a single country instead? See the Local client pack — most of the API is identical, but local transfers are a simpler single-step send with no FX or quoting.
responseCode, responseMessage and a data payload.Environments & variables
Replace the placeholders below with the values provided by your Tilt account manager.
| Variable | Description |
|---|---|
{baseUrl} | Tilt Pay API base URL for your environment |
{baseAuthUrl} | Authentication server base URL |
{username} / {password} | Your API credentials |
{client_id} / {audience} | OAuth client ID and audience for your tenant |
{account_institution} | Institution ID of your Tilt account (e.g. gh_tilt) |
{account_number} | Your Tilt account number |
Authorization: Bearer <access_token>.Authentication
Use your username and password to obtain an access token, which you need to authenticate your account when using the API.
# cURL curl -X POST "{baseAuthUrl}/oauth/token" \ -H "Content-Type: application/json" \ -d '{ "username": "{username}", "password": "{password}", "audience": "{audience}", "grant_type": "password", "client_id": "{client_id}" }'
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 86400
}
Resources
List available institutions for a country. Required for account verification and creating transfers.
| Parameter | In | Description | |
|---|---|---|---|
country | path | required | Country in lowercase ISO 3166-1 alpha-2 format (e.g. gh, zm) |
list | query | optional | Set true to return a flat id → name map |
type | query | optional | Filter by institution type, e.g. bank |
curl "{baseUrl}/v1/resources/institutions/gh/?list=true" \
-H "Authorization: Bearer $TOKEN"
{
"responseCode": 200,
"responseMessage": "Fetched institutions",
"data": {
"gh_tilt": "Tilt Ghana",
"gh_absa": "Absa Bank Ghana Limited",
"gh_mtn": "MTN Mobile Money",
"gh_ecobank": "Ecobank",
"gh_stanbic": "Stanbic Bank",
"gh_vodafone": "Vodafone Money",
"gh_zenith": "Zenith Bank"
// ... banks, mobile money and savings & loans institutions
}
}
FX
Fetch the current exchange rate between two currencies.
| Parameter | In | Description | |
|---|---|---|---|
from_currency | path | required | Source currency, lowercase ISO 4217 (e.g. usd) |
to_currency | path | required | Target currency, lowercase ISO 4217 (e.g. ngn) |
curl "{baseUrl}/v1/rates/fx/usd/ngn" \
-H "Authorization: Bearer $TOKEN"
{
"responseCode": 200,
"responseMessage": "Fetched currency exchange rate",
"data": {
"from": { "currency": "usd" },
"to": { "currency": "ngn" },
"rate": 1750.6489495892
}
}
Your account
Gets the real-time balance of an account that the requesting client has permission to view.
curl "{baseUrl}/v1/accounts/{account_institution}/{account_number}/balance" \
-H "Authorization: Bearer $TOKEN"
{
"responseCode": 200,
"responseMessage": "Fetched account balance",
"data": {
"balance": 10250.75,
"currency": "zmw"
}
}
Retrieve transaction data for an account that the requesting client has permission to view.
| Parameter | In | Description | |
|---|---|---|---|
from_datetime | query | optional | Start of range, e.g. 2024-07-09T00:00:00+0000 |
to_datetime | query | optional | End of range, e.g. 2024-07-11T00:00:00+0000 |
Accept | header | optional | Response format — application/json (default) or text/csv |
curl "{baseUrl}/v1/accounts/{account_institution}/{account_number}/transactions?from_datetime=2024-07-09T00:00:00%2B0000&to_datetime=2024-07-11T00:00:00%2B0000" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json"
Retrieve a list of available statements for an account that the requesting client has permission to view. Statement transaction data is fetched separately from the referenced link — it is not included when listing statements.
| Parameter | In | Description | |
|---|---|---|---|
from_datetime | query | optional | Filter start date/time. Open-ended if omitted |
to_datetime | query | optional | Filter end date/time. Open-ended if omitted |
search | query | optional | Free-text search, e.g. weekly |
curl "{baseUrl}/v1/accounts/{account_institution}/{account_number}/statements" \
-H "Authorization: Bearer $TOKEN"
{
"data": [
{
"statement_id": "stm_01HV...",
"opening_balance": 9500.00,
"closing_balance": 10250.75,
"transactions_count": 42
}
]
}
Retrieve a specific statement. Statement transaction data is included in the response when available, or a link is provided to an archived statement. Transaction data is generally available for 90 days before being archived.
Use the Accept header to specify the response format, such as text/csv.
curl "{baseUrl}/v1/accounts/{account_institution}/{account_number}/statements/{statement_id}" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: text/csv"
Withdraw funds from your Tilt account to a predefined bank account, registered with Tilt. Supply a unique external_transaction_id (UUID) for idempotency and optional webhook notifications.
curl -X POST "{baseUrl}/v1/accounts/{account_institution}/{account_number}/withdraw" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"instruction": {
"external_transaction_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"amount": 100,
"narration": "Send to bank",
"notifications": [
{
"on_status": ["ACCEPTED", "SUCCESSFUL", "FAILED"],
"to_url": "https://example.com/webhooks/tilt"
}
]
}
}'
The response includes a transfer_id, the final status, the echoed external_transaction_id, the instruction (debtor/creditor), and an actions array tracing each state change.
Lookup recipient account
Fetch information for an account number if the institution supports account discovery. Use this to verify a recipient before creating a transfer.
| Parameter | In | Description | |
|---|---|---|---|
institution | path | required | Institution identifier as found in the institutions API |
account_number | path | required | Account number (or mobile number for a wallet) to look up |
curl "{baseUrl}/v1/accounts/zm_airtel/260970000100" \
-H "Authorization: Bearer $TOKEN"
{
"data": {
"institution": "zm_airtel",
"account_number": "260970000100",
"status": "ACTIVE"
}
}
Transfers — Send IMT
The Transfers API allows you to send money directly into a recipient's account. There are two flows:
Auto-accept quote — POST /v1/transfers creates and immediately executes the transfer at the current rate.
Quote then settle — POST /v1/transfers/quote returns a quote first; you confirm it with PATCH /v1/transfers/:transfer_id/settle.
Create an IMT transfer (auto-accepting the quote). On creation the transfer status is PENDING; track it via webhooks or by fetching the transfer.
| Field | Description | |
|---|---|---|
instruction.debtor | required | Sender — institution, account_number, and kyc (first/last name, id_number, id_type e.g. passport, address, country) |
instruction.creditor | required | Recipient — institution, account_number, mobile_number, and kyc (id_type e.g. national_id) |
instruction.additional_details | required | purpose_of_funds (e.g. GIFT_AND_DONATION), origin_currency, origin_amount |
instruction.amount / currency | required | Destination amount and lowercase ISO 4217 currency (e.g. zmw) |
instruction.external_transaction_id | required | Your unique UUID for idempotency and reconciliation |
instruction.scheme | required | imt_transfer |
instruction.narration | optional | Free-text reference shown on the transaction |
instruction.notifications | optional | Webhook subscriptions — see Webhooks |
curl -X POST "{baseUrl}/v1/transfers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"instruction": {
"debtor": {
"institution": "{account_institution}",
"account_number": "{account_number}",
"kyc": {
"first_name": "Roy",
"last_name": "Milner",
"id_number": "111111",
"id_type": "passport",
"address": "address",
"country": "us"
}
},
"creditor": {
"institution": "zm_absa",
"account_number": "123456",
"mobile_number": "260960000001",
"kyc": {
"first_name": "Tanya",
"last_name": "Winslow",
"id_number": "222222222",
"id_type": "national_id"
}
},
"additional_details": {
"purpose_of_funds": "GIFT_AND_DONATION",
"origin_currency": "usd",
"origin_amount": "50"
},
"amount": 1000,
"currency": "zmw",
"narration": "test",
"external_transaction_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"scheme": "imt_transfer",
"notifications": [
{
"on_status": ["ACCEPTED", "QUOTED", "QUOTE_EXPIRED", "PENDING", "PENDING_RETRY",
"PENDING_KYC", "KYC_EXPIRED", "SUCCESSFUL", "FAILED"],
"to_url": "https://example.com/webhooks/tilt"
}
]
}
}'
{
"data": {
"transfer_id": "trf_01HV...",
"status": "PENDING",
"external_transaction_id": "9b1deb4d-...",
"instruction": { "debtor": { ... }, "creditor": { ... } },
"actions": [
{ "status": "ACCEPTED", "_meta": { "cause": "...", "trace_api_log_id": "..." } }
]
}
}
Create a quoted transfer without executing it. The request body is identical to POST /v1/transfers. The response returns a transfer_id and quote details — settle it with the endpoint below before the quote expires (QUOTE_EXPIRED).
curl -X POST "{baseUrl}/v1/transfers/quote" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ /* same instruction body as POST /v1/transfers */ }'
Accept and settle a previously quoted transfer, using the transfer_id returned by the quote call.
curl -X PATCH "{baseUrl}/v1/transfers/{transfer_id}/settle" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"
Verify whether a particular external_transaction_id has been received and processed by Tilt. Use this for reconciliation or before retrying after a timeout — it prevents duplicate sends.
curl "{baseUrl}/v1/transfers/verify/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" \
-H "Authorization: Bearer $TOKEN"
{
"responseMessage": "A request has been received for this external_transaction_id",
"data": { "transfer_id": "trf_01HV...", "status": "...", ... }
}
Fetch a transfer using the transfer_id issued by Tilt. The actions array lists every state change; the last action's status matches the transfer's current status.
curl "{baseUrl}/v1/transfers/{transfer_id}" \
-H "Authorization: Bearer $TOKEN"
Resend the last webhook notification for a transfer — useful when your endpoint was down or you need to replay an event.
curl -X POST "{baseUrl}/v1/transfers/{transfer_id}/resend-notification" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json"
{
"data": {
"webhook": { "request_id": "...", "url": "...", "message": { ... } },
"response": { "http_code": 200 }
}
}
Transfer statuses
A transfer moves through four phases: create, wait, process and complete. Subscribe to any status via the notifications array on the transfer instruction.
PENDING means the transfer has been sent to the credit institution and Tilt is waiting for an outcome. A transfer drops back to PENDING_RETRY when delivery must be re-attempted, and returns to PENDING once it is picked up for processing. PENDING_KYC is entered from ACCEPTED or QUOTED when the recipient must complete in-market KYC requirements before the transfer can proceed to PENDING.
PENDING_RETRY and PENDING_KYC states can be cancelled by the client.Webhooks
Attach a notifications array to any transfer or withdrawal instruction to receive HTTP callbacks as the transfer changes state:
"notifications": [ { "on_status": ["ACCEPTED", "PENDING_RETRY", "SUCCESSFUL", "FAILED"], "to_url": "https://example.com/webhooks/tilt" } ]
Subscribable statuses: ACCEPTED, QUOTED, QUOTE_EXPIRED, PENDING, PENDING_RETRY, PENDING_KYC, KYC_EXPIRED, SUCCESSFUL, FAILED.
Each webhook delivery includes a request_id, the destination url and the message payload. Missed a delivery? Replay it with resend-notification.
Sandbox mock samples
The sandbox environment includes magic account numbers that deterministically trigger specific outcomes, so you can exercise every branch of your integration. Only the last three digits of the account number matter — the institution and the leading digits can be anything, so these triggers work for any institution, not just the Zambian examples shown below. Any account number whose last three digits don't match one of the special values in the tables below returns a success response by default.
Recipient lookup — GET /v1/accounts/:institution/:account_number
| Last 3 digits | Outcome | Example |
|---|---|---|
600 | Account not found | zm_airtel / 0970000600 |
700 | Institution error | zm_airtel / 0970000700 |
| anything else | Success — account found | zm_airtel / 260970000100 |
Send IMT — POST /v1/transfers (creditor values)
| Last 3 digits | Outcome | Example |
|---|---|---|
200 | PENDING → SUCCESSFUL (async) | zm_mtn / 0961000200 |
300 | PENDING → FAILED (async) | zm_mtn / 0961000300 |
500 | FAILED — institution error | zm_fnb / 100500 |
600 | FAILED — account not found | zm_absa / 100600 |
700 | FAILED — account error | zm_mtn / 0961000700 |
| anything else | SUCCESSFUL | zm_absa / 123456 |
PENDING → samples to test your webhook handling end-to-end before going live.