Transfers API
Initiate single transfers programmatically. The API creates a transfer draft and hands it to your workspace's approval flow — a manager or legal representative reviews and approves it in the Zazu app. The API never moves money on its own: an API key alone cannot execute a transfer.
How it works#
POST /api/paymentsvalidates the payload and creates a draft inrequestedstatus. The workspace's managers and legal representatives are notified and the draft appears in their approval queue.- An approver reviews the draft in the app and authorizes it there (protected by their own two-factor step). Execution then follows the same path as any web-initiated transfer — settlement, audit trail, and webhooks behave identically.
- Poll
GET /api/payments/:id(or subscribe to thetransfer.executedwebhook) to follow the lifecycle:requested→processing→completed/failed.
The recipient is either a saved beneficiary (beneficiary_id, see Beneficiaries) or one of your own accounts (destination_account_id) for internal moves. Scheduled, recurring, and bulk transfers are not available via the API.
release_api_transfers feature flag on your workspace. Contact support to enable it.Required scopes#
| Endpoint | Scope |
|---|---|
| Initiate a transfer | transfers:write |
| Get transfer draft | transfers:read |
| List / Get beneficiaries | beneficiaries:read |
The member who created the API key must also hold the payments:create permission — the draft is created on their behalf and they appear as the requester in the approval queue and the audit trail.
Transfer draft object#
While approval is outstanding the status is requested and transfer is null. Once approved and executed, the draft carries the transfer's id and status:
{
"id": "01964a3b-1a2b-7000-8000-feed00000042",
"status": "completed",
"amount": "1500.00",
"currency_code": "ZAR",
"payment_reference": "INV-000042",
"account_id": "01964a3b-0000-7000-8000-ac6000000a01",
"beneficiary_id": "01964a3b-5e6f-7000-8000-beef00000001",
"external_account_id": "01964a3b-7a8b-7000-8000-beef00000002",
"destination_account_id": null,
"transfer": {
"id": "01964a3b-3c4d-7000-8000-feed00000043",
"status": "accepted"
},
"created_at": "2026-07-16T09:00:00Z",
"updated_at": "2026-07-16T10:41:30Z"
}List beneficiaries#
GET /api/beneficiaries
Read-only directory of your saved recipients, used to resolve the beneficiary_id for a transfer. Each beneficiary embeds its bank accounts; when a transfer names only the beneficiary, the account marked default is used. Beneficiaries are created and managed in the dashboard. Cursor pagination as elsewhere (limit 1–100, default 25). GET /api/beneficiaries/:id fetches one.
curl "https://zazu.africa/api/beneficiaries" \
-H "Authorization: Bearer sk_live_..."zazu beneficiaries listimport { Zazu } from "@getzazu/sdk";
const zazu = new Zazu({ apiKey: process.env.ZAZU_API_KEY });
await zazu.beneficiaries.list();require "zazu"
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"])
zazu.beneficiaries.listfrom zazu_sdk import Zazu
client = Zazu(api_key="sk_live_...")
client.beneficiaries.list()import zazu "github.com/getzazu/zazu-go"
client, _ := zazu.New(zazu.WithAPIKey(os.Getenv("ZAZU_API_KEY")))
page, err := client.Beneficiaries.List(ctx, zazu.ListParams{})let client = zazu_sdk::Client::builder()
.api_key(std::env::var("ZAZU_API_KEY")?)
.build()?;
let page = client.beneficiaries().list(Default::default())?;{:ok, client} = Zazu.new(api_key: System.fetch_env!("ZAZU_API_KEY"))
{:ok, page} = Zazu.Beneficiaries.list(client)require "zazu"
client = Zazu::Client.new # reads ZAZU_API_KEY
page = client.beneficiaries.listuse Zazu\Client;
$client = new Client(apiKey: getenv('ZAZU_API_KEY'));
$page = $client->beneficiaries->list();{
"data": [
{
"id": "01964a3b-5e6f-7000-8000-beef00000001",
"name": "Acme Supplies Ltd",
"beneficiary_type": "company",
"email": "billing@acme.example",
"phone_number": null,
"external_accounts": [
{
"id": "01964a3b-7a8b-7000-8000-beef00000002",
"name": "Acme Main",
"account_number": "62000003592",
"bank_identifier": "410105",
"currency_code": "ZAR",
"default": true
}
],
"created_at": "2026-05-02T10:15:00Z"
}
],
"has_more": false,
"next_cursor": null
}Initiate a transfer#
POST /api/payments
Required: account_id, amount, and exactly one of beneficiary_id or destination_account_id. Optional: external_account_id (a specific bank account of the beneficiary), currency_code (must match the source account), payment_reference, and internal_notes. A successful response is 201 with the draft in requested status — approval now happens in the app.
curl -X POST "https://zazu.africa/api/payments" \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"account_id": "01964a3b-0000-7000-8000-ac6000000a01",
"beneficiary_id": "01964a3b-5e6f-7000-8000-beef00000001",
"amount": "1500.00",
"payment_reference": "INV-000042"
}'zazu transfers create --account-id 01964a3b-0000-7000-8000-ac6000000a01 \
--beneficiary-id 01964a3b-5e6f-7000-8000-beef00000001 --amount 1500.00 --payment-reference "INV-000042"import { Zazu } from "@getzazu/sdk";
const zazu = new Zazu({ apiKey: process.env.ZAZU_API_KEY });
await zazu.payments.create({
accountId: "01964a3b-0000-7000-8000-ac6000000a01",
beneficiaryId: "01964a3b-5e6f-7000-8000-beef00000001",
amount: "1500.00",
paymentReference: "INV-000042",
});require "zazu"
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"])
zazu.payments.create(
account_id: "01964a3b-0000-7000-8000-ac6000000a01",
beneficiary_id: "01964a3b-5e6f-7000-8000-beef00000001",
amount: "1500.00",
payment_reference: "INV-000042"
)from zazu_sdk import Zazu
client = Zazu(api_key="sk_live_...")
client.payments.create(
account_id="01964a3b-0000-7000-8000-ac6000000a01",
beneficiary_id="01964a3b-5e6f-7000-8000-beef00000001",
amount="1500.00",
payment_reference="INV-000042",
)import zazu "github.com/getzazu/zazu-go"
client, _ := zazu.New(zazu.WithAPIKey(os.Getenv("ZAZU_API_KEY")))
draft, err := client.Payments.Create(ctx, zazu.Attributes{
"account_id": "01964a3b-0000-7000-8000-ac6000000a01",
"beneficiary_id": "01964a3b-5e6f-7000-8000-beef00000001",
"amount": "1500.00",
"payment_reference": "INV-000042",
})let client = zazu_sdk::Client::builder()
.api_key(std::env::var("ZAZU_API_KEY")?)
.build()?;
let draft = client.payments().create(&serde_json::json!({
"account_id": "01964a3b-0000-7000-8000-ac6000000a01",
"beneficiary_id": "01964a3b-5e6f-7000-8000-beef00000001",
"amount": "1500.00",
"payment_reference": "INV-000042",
}))?;{:ok, client} = Zazu.new(api_key: System.fetch_env!("ZAZU_API_KEY"))
{:ok, draft} =
Zazu.Payments.create(client, %{
"account_id" => "01964a3b-0000-7000-8000-ac6000000a01",
"beneficiary_id" => "01964a3b-5e6f-7000-8000-beef00000001",
"amount" => "1500.00",
"payment_reference" => "INV-000042"
})require "zazu"
client = Zazu::Client.new # reads ZAZU_API_KEY
draft = client.payments.create(
account_id: "01964a3b-0000-7000-8000-ac6000000a01",
beneficiary_id: "01964a3b-5e6f-7000-8000-beef00000001",
amount: "1500.00",
payment_reference: "INV-000042"
)use Zazu\Client;
$client = new Client(apiKey: getenv('ZAZU_API_KEY'));
$draft = $client->payments->create([
'account_id' => '01964a3b-0000-7000-8000-ac6000000a01',
'beneficiary_id' => '01964a3b-5e6f-7000-8000-beef00000001',
'amount' => '1500.00',
'payment_reference' => 'INV-000042',
]);{
"id": "01964a3b-1a2b-7000-8000-feed00000042",
"status": "requested",
"amount": "1500.00",
"currency_code": "ZAR",
"payment_reference": "INV-000042",
"account_id": "01964a3b-0000-7000-8000-ac6000000a01",
"beneficiary_id": "01964a3b-5e6f-7000-8000-beef00000001",
"external_account_id": "01964a3b-7a8b-7000-8000-beef00000002",
"destination_account_id": null,
"transfer": null,
"created_at": "2026-07-16T09:00:00Z",
"updated_at": "2026-07-16T09:00:00Z"
}Get a transfer draft#
GET /api/payments/:id
Poll a draft's status while it moves through approval and execution. Only drafts created through the API are visible here — transfers initiated in the dashboard are not.
curl "https://zazu.africa/api/payments/01964a3b-1a2b-7000-8000-feed00000042" \
-H "Authorization: Bearer sk_live_..."zazu transfers get 01964a3b-1a2b-7000-8000-feed00000042import { Zazu } from "@getzazu/sdk";
const zazu = new Zazu({ apiKey: process.env.ZAZU_API_KEY });
await zazu.payments.retrieve("01964a3b-1a2b-7000-8000-feed00000042");require "zazu"
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"])
zazu.payments.retrieve("01964a3b-1a2b-7000-8000-feed00000042")from zazu_sdk import Zazu
client = Zazu(api_key="sk_live_...")
client.payments.retrieve("01964a3b-1a2b-7000-8000-feed00000042")import zazu "github.com/getzazu/zazu-go"
client, _ := zazu.New(zazu.WithAPIKey(os.Getenv("ZAZU_API_KEY")))
draft, err := client.Payments.Get(ctx, "01964a3b-1a2b-7000-8000-feed00000042")let client = zazu_sdk::Client::builder()
.api_key(std::env::var("ZAZU_API_KEY")?)
.build()?;
let draft = client.payments().get("01964a3b-1a2b-7000-8000-feed00000042")?;{:ok, client} = Zazu.new(api_key: System.fetch_env!("ZAZU_API_KEY"))
{:ok, draft} = Zazu.Payments.get(client, "01964a3b-1a2b-7000-8000-feed00000042")require "zazu"
client = Zazu::Client.new # reads ZAZU_API_KEY
draft = client.payments.get("01964a3b-1a2b-7000-8000-feed00000042")use Zazu\Client;
$client = new Client(apiKey: getenv('ZAZU_API_KEY'));
$draft = $client->payments->get('01964a3b-1a2b-7000-8000-feed00000042');{
"id": "01964a3b-1a2b-7000-8000-feed00000042",
"status": "completed",
"amount": "1500.00",
"currency_code": "ZAR",
"payment_reference": "INV-000042",
"account_id": "01964a3b-0000-7000-8000-ac6000000a01",
"beneficiary_id": "01964a3b-5e6f-7000-8000-beef00000001",
"external_account_id": "01964a3b-7a8b-7000-8000-beef00000002",
"destination_account_id": null,
"transfer": {
"id": "01964a3b-3c4d-7000-8000-feed00000043",
"status": "accepted"
},
"created_at": "2026-07-16T09:00:00Z",
"updated_at": "2026-07-16T10:41:30Z"
}Error responses#
| Scenario | Status | Type | Param |
|---|---|---|---|
| Unknown account / beneficiary / destination | 422 | validation_error | account_id |
| Both or neither recipient field given | 422 | validation_error | beneficiary_id |
| Amount ≤ 0 or not a number | 422 | validation_error | amount |
| Amount exceeds available balance | 422 | insufficient_funds | — |
| Key's creator may not create transfer drafts | 403 | forbidden_error | — |
| API transfers not enabled | 403 | forbidden_error | — |
| Initiation rate limit exceeded | 429 | rate_limit_error | — |