---
title: "Send money from your KES account"
method: POST
path: "/v1/transfers"
tags: ["Partner API"]
---

# Send money from your KES account

`POST /v1/transfers`

## Request

- Base URL: `https://sandbox.onlink.africa`
- URL: `https://sandbox.onlink.africa/v1/transfers`
- Auth: API key in header `X-OnLink-Key`

Validates the destination, verifies the beneficiary name where the rail supports it, checks your caps and balance, then writes the instruction and sends a confirmation code to your registered confirmation contact, or to your webhook as confirmation.code_issued if OnLink has switched your account to webhook delivery. Nothing reaches the bank until that code is verified via POST /v1/confirmations/:id, unless OnLink has enabled confirmation waiving for your account and, in production, the API key has an IP allowlist, in which case the transfer is returned already confirmed and is executed immediately.

## Request body

- PartnerCreateTransferDto
  - `rail` 'mpesa' | 'pesalink' | 'rtgs' | 'eft' | 'swift' | 'internal', required: The rail to send on.
  - `destination` TransferDestinationDto, required
    - `accountNumber` string, required: Beneficiary account number. For mpesa, a Kenyan mobile number (any recognised local or international format — normalised server-side); for the other rails, the beneficiary account number.
    - `accountName` string, required: Beneficiary name as you believe it to be — verified server-side against the rail before the transfer is created.
    - `bankCode` string: Beneficiary bank code. Required for pesalink, rtgs, eft. Not allowed for mpesa, swift, internal.
    - `bankName` string: Beneficiary bank name. Required for rtgs, eft, swift. Not allowed for mpesa, pesalink, internal.
    - `branchCode` string: Beneficiary bank branch code. Optional for rtgs, eft. Not allowed for mpesa, pesalink, swift, internal.
    - `swift` SwiftDestinationDto
      - `bic` string, required: Beneficiary bank BIC/SWIFT code (8 or 11 characters).
      - `bankCity` string, required: Beneficiary bank city.
      - `bankCountry` string, required: Beneficiary bank country, spelled out in FULL — never an ISO code.
      - `chargeType` 'OUR' | 'BEN' | 'SHA': Who bears the SWIFT charge. Defaults to OUR (you pay).
      - `accountCurrency` string: ISO currency of the beneficiary account. Defaults to the send currency. A USD transfer must pay a USD account.
  - `amount` TransferAmountDto, required
    - `currency` 'KES' | 'USD', required: Send currency, and the account it is paid from: KES from your KES account, USD from your USD account. Nothing is converted. USD is accepted on rtgs, eft and swift only (to a USD beneficiary account); any other rail refuses it with TRANSFER_RAIL_CURRENCY_UNSUPPORTED. USD is for Collections accounts only: a USDT Rails account is refused TRANSFER_CURRENCY_NOT_ENABLED on any rail.
    - `value` string, required: Decimal string, major units of `currency`, up to 2 decimal places, GREATER than zero. Never a JSON number.
  - `partnerReference` string: Your own reference for this transfer, echoed back on every read.
  - `idempotencyKey` string, required: A key unique to your account. A retried create with the same key and the same request body returns the original transfer rather than creating a second one; the same key with a DIFFERENT request body is refused.
  - `paymentPurposeId` string: Payment purpose code. Required for rtgs, eft, swift. Not allowed for mpesa, pesalink, internal.
  - `senderAddress` string: Your registered address. Required for rtgs, eft, swift. Not allowed for mpesa, pesalink, internal.
  - `remark` string: Optional free-text remark. Allowed on every rail.

## Response `202`

The instruction was created. Normally a confirmation code is sent and the response carries a `confirmation` object. When OnLink has enabled confirmation waiving for your account and, in production, the API key has an IP allowlist, the transfer is created already `confirmed`, executes immediately, and the response carries no `confirmation` object.

- PartnerTransferResponseDto
  - `id` string, required: The transfer id.
  - `status` 'awaiting_confirmation' | 'confirmed' | 'executing' | 'processing' | 'settled' | 'failed' | 'expired' | 'cancelled', required: The transfer status.
  - `rail` 'mpesa' | 'pesalink' | 'rtgs' | 'eft' | 'swift' | 'internal', required: The rail this transfer moves on.
  - `amount` TransferAmountDto, required
    - `currency` 'KES' | 'USD', required: Send currency, and the account it is paid from: KES from your KES account, USD from your USD account. Nothing is converted. USD is accepted on rtgs, eft and swift only (to a USD beneficiary account); any other rail refuses it with TRANSFER_RAIL_CURRENCY_UNSUPPORTED. USD is for Collections accounts only: a USDT Rails account is refused TRANSFER_CURRENCY_NOT_ENABLED on any rail.
    - `value` string, required: Decimal string, major units of `currency`, up to 2 decimal places, GREATER than zero. Never a JSON number.
  - `fee` TransferAmountDto, required
    - `currency` 'KES' | 'USD', required: Send currency, and the account it is paid from: KES from your KES account, USD from your USD account. Nothing is converted. USD is accepted on rtgs, eft and swift only (to a USD beneficiary account); any other rail refuses it with TRANSFER_RAIL_CURRENCY_UNSUPPORTED. USD is for Collections accounts only: a USDT Rails account is refused TRANSFER_CURRENCY_NOT_ENABLED on any rail.
    - `value` string, required: Decimal string, major units of `currency`, up to 2 decimal places, GREATER than zero. Never a JSON number.
  - `destination` TransferDestinationEchoDto, required
    - `accountNumber` string, required: The account number you supplied.
    - `accountName` string, required: The account name you supplied.
    - `bankCode` string: The bank code you supplied, when the rail carries one.
  - `nameVerification` 'verified' | 'unavailable', required: Whether the beneficiary name could be verified against the rail. `unavailable` covers both "this rail has no verification" (rtgs, eft, swift) and a provider outage — see verifiedAccountName.
  - `verifiedAccountName` string: The resolved beneficiary name, present only when nameVerification is verified.
  - `partnerReference` string: Your own reference, echoed back.
  - `failureCode` string: A classified failure code, present only once failed.
  - `createdAt` string, date-time, required: ISO-8601 instant the transfer was created.
  - `confirmedAt` string, date-time: ISO-8601 instant the transfer was confirmed: when its confirmation code was verified, or, when confirmation is waived for your account, when the transfer was created already confirmed.
  - `settledAt` string, date-time: ISO-8601 instant the transfer settled.
  - `confirmation` ConfirmationDto
    - `id` string, required: The confirmation id. POST the code your confirmation contact receives (or your webhook, as `confirmation.code_issued`, when `channel` is `webhook`) back to `/v1/confirmations/:id` to authorise the transfer.
    - `channel` 'email' | 'sms' | 'webhook', required: How the confirmation code was delivered. `webhook` means it was pushed to your webhook endpoint as `confirmation.code_issued`.
    - `deliveredTo` string, required: Masked delivery address or number. Never the raw contact.
    - `expiresAt` string, date-time, required: ISO-8601 instant the confirmation code expires.
    - `resendAvailableAt` string, date-time, required: ISO-8601 instant a resend becomes available.
    - `webhookDelivery` ConfirmationWebhookDeliveryDto
      - `status` 'pending' | 'delivered' | 'failed', required: `pending` while we are still trying, `delivered` once your endpoint acknowledged the push, `failed` once every attempt has failed.
      - `attempts` integer, required: How many push attempts have been made so far.
      - `lastAttemptAt` string, date-time, nullable, required: ISO-8601 instant of the most recent push attempt, or null before the first.
    - `waiverNotApplied` 'key_without_ip_allowlist': Present only when OnLink has enabled confirmation waiving for your account but this request still needs a confirmation code, because the API key that made it has no IP allowlist. Ask OnLink to add an IP allowlist to that API key; payouts made with it are then created already confirmed. Present on the first response only: a repeat of the same request (an idempotent replay) returns the stored payout without it. Absent in every other case.

## Other responses

- `404`: PARTNER_NOT_FOUND.
- `409`: PARTNER_SUSPENDED or TRANSFER_IDEMPOTENCY_CONFLICT.
- `422`: TRANSFER_AMOUNT_INVALID, TRANSFER_CURRENCY_NOT_ENABLED, TRANSFER_RAIL_CURRENCY_UNSUPPORTED, TRANSFER_DESTINATION_INVALID, TRANSFER_NAME_MISMATCH, TRANSFER_EXCEEDS_PER_ORDER_CAP, TRANSFER_EXCEEDS_DAILY_CAP, or TRANSFER_INSUFFICIENT_BALANCE.
- `503`: PARTNER_TRANSFERS_DISABLED, MONEY_MOVEMENT_PAUSED, CONFIRMATION_CONTACT_MISSING, CONFIRMATION_DELIVERY_UNAVAILABLE, or one of the VA resolver refusals (PARTNER_VA_NOT_ALLOCATED / _ACTIVATING / _SUSPENDED). The two CONFIRMATION_* refusals mean no confirmation code can be delivered; when the cause is a waived account whose API key has no IP allowlist, the body also carries `details.waiverNotApplied: "key_without_ip_allowlist"`. Nothing was created.

---

[API](https://skmtc.dev/onlink/apis/onlink-partner-api.md) - [All operations](https://skmtc.dev/onlink/apis/onlink-partner-api/llms.txt) - [OpenAPI document](https://skmtc.dev/onlink/apis/onlink-partner-api/revisions/1d1c6a3671f2?raw)
