REST API

# 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

1. `POST /api/payments` validates the payload and creates a draft in `requested` status. The workspace's managers and legal representatives are notified and the draft appears in their approval queue.
2. 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](https://zazu.africa/docs/webhooks) behave identically.
3. Poll `GET /api/payments/:id` (or subscribe to the `transfer.executed` webhook) to follow the lifecycle: `requested` → `processing` → `completed` / `failed`.

The recipient is either a saved beneficiary (`beneficiary_id`, see [Beneficiaries](#beneficiaries)) or one of your own accounts (`destination_account_id`) for internal moves. Scheduled, recurring, and bulk transfers are not available via the API.

> **Feature-flagged:** API transfers require the `release_api_transfers` feature flag on your workspace. Contact support to enable it.

> **Rate limit:** Transfer initiation has its own limit of 10 requests per minute per API key, on top of the global 120/minute.

## 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:

```json
{
  "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

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl "https://zazu.africa/api/beneficiaries" \
  -H "Authorization: Bearer sk_live_..."
```

```console
zazu beneficiaries list
```

```javascript
import { Zazu } from "@getzazu/sdk";
const zazu = new Zazu({ apiKey: process.env.ZAZU_API_KEY });

await zazu.beneficiaries.list();
```

```ruby
require "zazu"
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"])

zazu.beneficiaries.list
```

```python
from zazu_sdk import Zazu
client = Zazu(api_key="sk_live_...")

client.beneficiaries.list()
```

```go
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{})
```

```rust
let client = zazu_sdk::Client::builder()
    .api_key(std::env::var("ZAZU_API_KEY")?)
    .build()?;

let page = client.beneficiaries().list(Default::default())?;
```

```elixir
{:ok, client} = Zazu.new(api_key: System.fetch_env!("ZAZU_API_KEY"))

{:ok, page} = Zazu.Beneficiaries.list(client)
```

```crystal
require "zazu"

client = Zazu::Client.new # reads ZAZU_API_KEY

page = client.beneficiaries.list
```

```php
use Zazu\Client;

$client = new Client(apiKey: getenv('ZAZU_API_KEY'));

$page = $client->beneficiaries->list();
```

```json
{
  "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

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
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"
  }'
```

```console
zazu transfers create --account-id 01964a3b-0000-7000-8000-ac6000000a01 \
  --beneficiary-id 01964a3b-5e6f-7000-8000-beef00000001 --amount 1500.00 --payment-reference "INV-000042"
```

```javascript
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",
});
```

```ruby
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"
)
```

```python
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",
)
```

```go
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",
})
```

```rust
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",
}))?;
```

```elixir
{: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"
  })
```

```crystal
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"
)
```

```php
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',
]);
```

```json
{
  "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

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl "https://zazu.africa/api/payments/01964a3b-1a2b-7000-8000-feed00000042" \
  -H "Authorization: Bearer sk_live_..."
```

```console
zazu transfers get 01964a3b-1a2b-7000-8000-feed00000042
```

```javascript
import { Zazu } from "@getzazu/sdk";
const zazu = new Zazu({ apiKey: process.env.ZAZU_API_KEY });

await zazu.payments.retrieve("01964a3b-1a2b-7000-8000-feed00000042");
```

```ruby
require "zazu"
zazu = Zazu.new(api_key: ENV["ZAZU_API_KEY"])

zazu.payments.retrieve("01964a3b-1a2b-7000-8000-feed00000042")
```

```python
from zazu_sdk import Zazu
client = Zazu(api_key="sk_live_...")

client.payments.retrieve("01964a3b-1a2b-7000-8000-feed00000042")
```

```go
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")
```

```rust
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")?;
```

```elixir
{:ok, client} = Zazu.new(api_key: System.fetch_env!("ZAZU_API_KEY"))

{:ok, draft} = Zazu.Payments.get(client, "01964a3b-1a2b-7000-8000-feed00000042")
```

```crystal
require "zazu"

client = Zazu::Client.new # reads ZAZU_API_KEY

draft = client.payments.get("01964a3b-1a2b-7000-8000-feed00000042")
```

```php
use Zazu\Client;

$client = new Client(apiKey: getenv('ZAZU_API_KEY'));

$draft = $client->payments->get('01964a3b-1a2b-7000-8000-feed00000042');
```

```json
{
  "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` | — |