Tilt Pay API · Client Pack (Local)
Send domestic transfers and collect payments within a single country 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 Local (domestic) transfer flow: authenticating, discovering institutions, looking up recipients, and creating and tracking transfers within a single country. Most of the API surface is identical to the IMT client pack — the only real difference is the transfer itself: local transfers are a single-step send with no FX, no quoting, and no cross-border KYC.
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. zw_tilt) |
{account_number} | Your Tilt account number |
{country} | Country your account and recipients operate in, lowercase ISO 3166-1 alpha-2 (e.g. zw) |
{currency} | Your account's lowercase ISO 4217 currency (e.g. zwl) |
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. Filter down to a specific institution type with tag or tags.
| Parameter | In | Description | |
|---|---|---|---|
country | path | required | Country in lowercase ISO 3166-1 alpha-2 format (e.g. zw) |
list | query | optional | Set true to return a flat id → name map |
tag | query | optional | Filter to a single institution type, e.g. bank |
tags | query | optional | Filter to an institution type, e.g. mobile_money |
curl "{baseUrl}/v1/resources/institutions/zw?list=true" \
-H "Authorization: Bearer $TOKEN"
curl "{baseUrl}/v1/resources/institutions/zw?tag=bank&list=true" \
-H "Authorization: Bearer $TOKEN"
curl "{baseUrl}/v1/resources/institutions/zw?tags=mobile_money&list=true" \
-H "Authorization: Bearer $TOKEN"
{
"responseCode": 200,
"responseMessage": "Fetched institutions",
"data": {
"zw_tilt": "Tilt Zimbabwe",
"zw_bancabc": "BancABC",
"zw_cbz": "CBZ Bank",
"zw_ecocash": "EcoCash",
"zw_onemoney": "OneMoney"
// ... banks and mobile money institutions
}
}
Fetch the details of a single institution by its identifier.
| Parameter | In | Description | |
|---|---|---|---|
institution | path | required | Institution identifier as found in the institutions list |
curl "{baseUrl}/v1/resources/institutions/zw_bancabc" \
-H "Authorization: Bearer $TOKEN"
{
"data": {
"id": "zw_bancabc",
"name": "BancABC",
"country": "zw",
"tags": ["bank"]
}
}
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": "zwl"
}
}
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. 2022-05-30T14:50:00+0000 |
to_datetime | query | optional | End of range, e.g. 2022-06-01T00: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=2022-05-30T14:50:00%2B0000&to_datetime=2022-06-01T00: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/zw_bancabc/123456789" \
-H "Authorization: Bearer $TOKEN"
{
"data": {
"institution": "zw_bancabc",
"account_number": "123456789",
"status": "ACTIVE"
}
}
Transfers — Send local transfer
The Transfers API lets you send money directly into a recipient's account within the same country. Local transfers are a single-step flow — unlike IMT, there's no quote/settle step, no FX conversion, and no cross-border KYC objects on the debtor or creditor.
Create a local transfer. On creation the transfer status is PENDING; track it via webhooks or by fetching the transfer.
| Field | Description | |
|---|---|---|
instruction.debtor | required | Sender — institution and account_number |
instruction.creditor | required | Recipient — institution and account_number (or mobile number for a wallet) |
instruction.amount / currency | required | Transfer amount and lowercase ISO 4217 currency (e.g. zwl) |
instruction.external_transaction_id | required | Your unique UUID for idempotency and reconciliation |
instruction.scheme | required | local_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}"
},
"creditor": {
"institution": "zw_bancabc",
"account_number": "123456789"
},
"amount": 10,
"currency": "{currency}",
"narration": "test",
"external_transaction_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"scheme": "local_transfer",
"notifications": [
{
"on_status": ["ACCEPTED", "PENDING", "PENDING_RETRY", "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": "..." } }
]
}
}
additional_details or kyc. Those fields only apply to the IMT flow. Local transfers only need debtor/creditor institution and account number.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 local 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. There's no quoting or in-market KYC step for local transfers, so PENDING_RETRY is the only wait state on the way to a terminal status.
PENDING_RETRY state can be cancelled by the client.SUCCESSFUL and FAILED are terminal — a transfer that reaches one of them will not change state again.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, PENDING, PENDING_RETRY, 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 local transfer — POST /v1/transfers (creditor values)
| Last 3 digits | Outcome | Example |
|---|---|---|
200 | PENDING → SUCCESSFUL (async) | zm_airtel / 0970000200 |
300 | PENDING → FAILED (async) | zm_airtel / 0970000300 |
500 | FAILED — institution error | zm_airtel / 0970000500 |
600 | FAILED — account not found | zm_airtel / 0970000600 |
700 | FAILED — account error | zm_airtel / 0970000700 |
| anything else | SUCCESSFUL | zm_airtel / 0970000100 |
PENDING → samples to test your webhook handling end-to-end before going live.