Tilt Pay API · Client Pack (Local)

Send domestic transfers and collect payments within a single country through the Tilt platform. Instant. Secure. Scalable.

REST · JSON OAuth 2.0 Bearer Domestic / Local Transfers Webhooks Need cross-border? See the IMT pack → ⤓ Download Postman collection

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.

Response envelope. Every JSON response carries the same shape: responseCode, responseMessage and a data payload.

Environments & variables

Replace the placeholders below with the values provided by your Tilt account manager.

VariableDescription
{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)
Authentication. All endpoints except Get token require a Bearer token: Authorization: Bearer <access_token>.

Authentication

POST{baseAuthUrl}/oauth/token

Use your username and password to obtain an access token, which you need to authenticate your account when using the API.

Request
# 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}"
  }'
Response · 200
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 86400
}
Cache the token and reuse it until it expires — don't request a new token per call.

Resources

GET/v1/resources/institutions/:country

List available institutions for a country. Required for account verification and creating transfers. Filter down to a specific institution type with tag or tags.

ParameterInDescription
countrypathrequiredCountry in lowercase ISO 3166-1 alpha-2 format (e.g. zw)
listqueryoptionalSet true to return a flat id → name map
tagqueryoptionalFilter to a single institution type, e.g. bank
tagsqueryoptionalFilter to an institution type, e.g. mobile_money
Request — all institutions
curl "{baseUrl}/v1/resources/institutions/zw?list=true" \
  -H "Authorization: Bearer $TOKEN"
Request — banks only
curl "{baseUrl}/v1/resources/institutions/zw?tag=bank&list=true" \
  -H "Authorization: Bearer $TOKEN"
Request — mobile money only
curl "{baseUrl}/v1/resources/institutions/zw?tags=mobile_money&list=true" \
  -H "Authorization: Bearer $TOKEN"
Response · 200 (truncated)
{
  "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
  }
}
GET/v1/resources/institutions/:institution

Fetch the details of a single institution by its identifier.

ParameterInDescription
institutionpathrequiredInstitution identifier as found in the institutions list
Request
curl "{baseUrl}/v1/resources/institutions/zw_bancabc" \
  -H "Authorization: Bearer $TOKEN"
Response · 200 (shape)
{
  "data": {
    "id": "zw_bancabc",
    "name": "BancABC",
    "country": "zw",
    "tags": ["bank"]
  }
}

Your account

GET/v1/accounts/:institution/:account_number/balance

Gets the real-time balance of an account that the requesting client has permission to view.

Request
curl "{baseUrl}/v1/accounts/{account_institution}/{account_number}/balance" \
  -H "Authorization: Bearer $TOKEN"
Response · 200
{
  "responseCode": 200,
  "responseMessage": "Fetched account balance",
  "data": {
    "balance": 10250.75,
    "currency": "zwl"
  }
}
GET/v1/accounts/:institution/:account_number/transactions

Retrieve transaction data for an account that the requesting client has permission to view.

ParameterInDescription
from_datetimequeryoptionalStart of range, e.g. 2022-05-30T14:50:00+0000
to_datetimequeryoptionalEnd of range, e.g. 2022-06-01T00:00:00+0000
AcceptheaderoptionalResponse format — application/json (default) or text/csv
Request
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"
Retention. Transaction data is available for up to 90 days, after which historical data must be requested from your account manager. Opening and closing balances are calculated nightly and are not available for transactions on the current day.
GET/v1/accounts/:institution/:account_number/statements

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.

ParameterInDescription
from_datetimequeryoptionalFilter start date/time. Open-ended if omitted
to_datetimequeryoptionalFilter end date/time. Open-ended if omitted
searchqueryoptionalFree-text search, e.g. weekly
Request
curl "{baseUrl}/v1/accounts/{account_institution}/{account_number}/statements" \
  -H "Authorization: Bearer $TOKEN"
Response · 200 (shape)
{
  "data": [
    {
      "statement_id": "stm_01HV...",
      "opening_balance": 9500.00,
      "closing_balance": 10250.75,
      "transactions_count": 42
    }
  ]
}
GET/v1/accounts/:institution/:account_number/statements/:statement_id

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.

Request
curl "{baseUrl}/v1/accounts/{account_institution}/{account_number}/statements/{statement_id}" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Accept: text/csv"
POST/v1/accounts/:institution/:account_number/withdraw

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.

Request
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

GET/v1/accounts/:institution/:account_number

Fetch information for an account number if the institution supports account discovery. Use this to verify a recipient before creating a transfer.

ParameterInDescription
institutionpathrequiredInstitution identifier as found in the institutions API
account_numberpathrequiredAccount number (or mobile number for a wallet) to look up
Request
curl "{baseUrl}/v1/accounts/zw_bancabc/123456789" \
  -H "Authorization: Bearer $TOKEN"
Response · 200 (shape)
{
  "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.

POST/v1/transfers

Create a local transfer. On creation the transfer status is PENDING; track it via webhooks or by fetching the transfer.

FieldDescription
instruction.debtorrequiredSender — institution and account_number
instruction.creditorrequiredRecipient — institution and account_number (or mobile number for a wallet)
instruction.amount / currencyrequiredTransfer amount and lowercase ISO 4217 currency (e.g. zwl)
instruction.external_transaction_idrequiredYour unique UUID for idempotency and reconciliation
instruction.schemerequiredlocal_transfer
instruction.narrationoptionalFree-text reference shown on the transaction
instruction.notificationsoptionalWebhook subscriptions — see Webhooks
Request
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"
        }
      ]
    }
  }'
Response · 200/201 (shape)
{
  "data": {
    "transfer_id": "trf_01HV...",
    "status": "PENDING",
    "external_transaction_id": "9b1deb4d-...",
    "instruction": { "debtor": { ... }, "creditor": { ... } },
    "actions": [
      { "status": "ACCEPTED", "_meta": { "cause": "...", "trace_api_log_id": "..." } }
    ]
  }
}
No additional_details or kyc. Those fields only apply to the IMT flow. Local transfers only need debtor/creditor institution and account number.
GET/v1/transfers/verify/:external_transaction_id

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.

Request
curl "{baseUrl}/v1/transfers/verify/9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" \
  -H "Authorization: Bearer $TOKEN"
Response · 200
{
  "responseMessage": "A request has been received for this external_transaction_id",
  "data": { "transfer_id": "trf_01HV...", "status": "...", ... }
}
GET/v1/transfers/:transfer_id

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.

Request
curl "{baseUrl}/v1/transfers/{transfer_id}" \
  -H "Authorization: Bearer $TOKEN"
POST/v1/transfers/:transfer_id/resend-notification

Resend the last webhook notification for a transfer — useful when your endpoint was down or you need to replay an event.

Request
curl -X POST "{baseUrl}/v1/transfers/{transfer_id}/resend-notification" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json"
Response · 200 (shape)
{
  "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.

1 · CREATE 2 · WAIT client may cancel 3 · PROCESS with credit institution 4 · COMPLETE final statuses retry picked up ACCEPTED PENDING_RETRY ✕ cancellable PENDING SUCCESSFUL FAILED

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.

Transfers in the PENDING_RETRY state can be cancelled by the client.
Final statuses. 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 digitsOutcomeExample
600Account not foundzm_airtel / 0970000600
700Institution errorzm_airtel / 0970000700
anything elseSuccess — account foundzm_airtel / 260970000100

Send local transfer — POST /v1/transfers (creditor values)

Last 3 digitsOutcomeExample
200PENDING → SUCCESSFUL (async)zm_airtel / 0970000200
300PENDING → FAILED (async)zm_airtel / 0970000300
500FAILED — institution errorzm_airtel / 0970000500
600FAILED — account not foundzm_airtel / 0970000600
700FAILED — account errorzm_airtel / 0970000700
anything elseSUCCESSFULzm_airtel / 0970000100
Use the async PENDING → samples to test your webhook handling end-to-end before going live.