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 Manza 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://ma.manza.dev/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.

> **Approving from your own system:** Everything above is the default flow. When your workspace has enrolled a webhook endpoint for transfer authorization, a transfer to a trusted payee within its limits skips the approval queue and executes once your endpoint signs it. See [Transfer authorization](https://ma.manza.dev/docs/api/transfer-authorization).

## Required scopes

| Endpoint | Scope |
| --- | --- |
| Initiate a transfer | `transfers:write` |
| Get transfer draft | `transfers:read` |
| Authorize / decline a transfer (separate key) | `transfers:authorize` |
| List / Get beneficiaries and bank accounts | `beneficiaries:read` |
| Create beneficiaries / bank accounts | `beneficiaries:write` |
| Request trust for payees | `beneficiaries:request_trust` |

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": "MAD",
  "payment_reference": "INV000042",
  "client_reference": null,
  "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"
  },
  "authorization": null,
  "created_at": "2026-07-16T09:00:00Z",
  "updated_at": "2026-07-16T10:41:30Z"
}
```

## List beneficiaries

`GET` `/api/beneficiaries`

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. Create recipients through the API below or in the dashboard. Recipients created by a bulk payment file are listed too, and can be paid and given bank accounts like any other. 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://ma.manza.dev/api/beneficiaries" \
  -H "Authorization: Bearer sk_live_..."
```

```console
manza beneficiaries list
```

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

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

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.beneficiaries.list
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.beneficiaries.list()
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

page, err := client.Beneficiaries.List(ctx, manza.ListParams{})
```

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

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

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

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

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

page = client.beneficiaries.list
```

```php
use Manza\Client;

$client = new Client(apiKey: getenv('MANZA_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": "MAD",
          "default": true
        }
      ],
      "created_at": "2026-05-02T10:15:00Z"
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

## Create a beneficiary

`POST` `/api/beneficiaries`

Requires `beneficiaries:write`, the `release_api_transfers` feature enabled for your workspace, and the API key creator's current permission to create beneficiaries. Existing read-only keys do not gain write access automatically.

Send `person_name` for an individual or `company_name` for a business. The type is inferred when only one name is supplied. If sending both names, specify `beneficiary_type` (`individual` or `business`). Optional: `email`, `phone_number`.

Returns **201** with the beneficiary `id`, `name`, `beneficiary_type`, contact fields, `created_at`, and an empty `external_accounts` array. Save the `id` for the next request. Invalid identity fields return **422**; missing scope, feature access or member permission returns **403**. Both creation endpoints share a limit of 10 creation requests per minute per API key; exceeding it returns **429**. Send field values as JSON strings; invalid value types return **400**.

```shell
curl -X POST "https://ma.manza.dev/api/beneficiaries" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"company_name":"Supplier Ltd","email":"accounts@example.com"}'
```

## Add a bank account

`POST` `/api/beneficiaries/:beneficiary_id/external_accounts`

Requires `beneficiaries:write`, the `release_api_transfers` feature enabled for your workspace, and the API key creator's current permission to create external accounts. The beneficiary must belong to the same workspace and be unarchived, including one created by a bulk payment file. An unavailable beneficiary returns **404**.

Send `account_number`; ZA also requires `bank_identifier` as a six-digit branch code. In Morocco, send the full 24-digit RIB as `account_number` and omit `bank_identifier`; supplying that field returns **422** because the bank identifier is derived from the RIB. Account numbers need at least six digits; spaces and hyphens are normalized. Letters and wallet phone numbers are rejected. Existing market-specific RIB and name validation applies.

Optional: `name` (account-holder name, defaults to the beneficiary name), `country_code` and `currency_code` (default to this deployment's market and configured currency). The account-holder name must be 2–64 characters. If the beneficiary name falls outside those limits, supply a valid `name` explicitly. Only bank accounts are supported: omit `account_type` or set it to `bank`. Use strings for account numbers and branch codes to preserve leading zeroes.

Returns **201** with `id`, `name`, `account_number`, `bank_identifier`, `currency_code` and `default`, matching the embedded account in the beneficiary response. The first account becomes the default; adding another does not replace it. Invalid or duplicate bank details return **422**; a duplicate names the beneficiary that already holds the account, which `GET /api/beneficiaries` lists. No bank ownership verification is implied by creation.

Pass the beneficiary `id` and returned account `id` as `beneficiary_id` and `external_account_id` to `POST /api/payments`. An approver approves the payment in the app, unless it qualifies for [transfer authorization](https://ma.manza.dev/docs/api/transfer-authorization). These creation endpoints do not provide request idempotency; inspect saved recipients after a timeout before resubmitting using the read endpoints below.

```shell
curl -X POST "https://ma.manza.dev/api/beneficiaries/01964a3b-5e6f-7000-8000-beef00000001/external_accounts" \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Supplier Ltd","account_number":"007810000102300012345697","country_code":"MA","currency_code":"MAD"}'
```

## List bank accounts

`GET` `/api/beneficiaries/:beneficiary_id/external_accounts`

Requires `beneficiaries:read` and `release_api_transfers`. Returns the beneficiary's unarchived accounts using the standard `data`, `has_more` and `next_cursor` pagination envelope. Use `limit` and `cursor` to page through results. Accounts belong to the authenticated workspace; an unavailable beneficiary returns **404**.

## Get a bank account

`GET` `/api/beneficiaries/:beneficiary_id/external_accounts/:id`

Requires `beneficiaries:read` and `release_api_transfers`. Returns an account object whose fields match the accounts included in beneficiary responses. The account must belong to the specified beneficiary in your workspace. An unavailable beneficiary or account returns **404**.

## 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` (in South Africa at most 18 characters, ASCII letters, numbers and spaces only; accented letters are not allowed), `internal_notes`, and `client_reference` (your own id, at most 128 characters and unique in your workspace; a repeat returns `409` with the existing transfer's `payment_id`). A successful response is `201` with the draft in `requested` status. An approver approves it in the app, or your enrolled endpoint approves it when it qualifies for [transfer authorization](https://ma.manza.dev/docs/api/transfer-authorization).

cURL

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

```console
curl -X POST "https://ma.manza.dev/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": "INV000042"
  }'
```

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

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.transferDrafts.create({
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
  beneficiary_id: "01964a3b-5e6f-7000-8000-beef00000001",
  amount: "1500.00",
  payment_reference: "INV000042",
});
```

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.transfer_drafts.create(
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
  beneficiary_id: "01964a3b-5e6f-7000-8000-beef00000001",
  amount: "1500.00",
  payment_reference: "INV000042"
)
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.transfer_drafts.create(
  account_id="01964a3b-0000-7000-8000-ac6000000a01",
  beneficiary_id="01964a3b-5e6f-7000-8000-beef00000001",
  amount="1500.00",
  payment_reference="INV000042",
)
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

draft, err := client.TransferDrafts.Create(ctx, manza.Attributes{
    "account_id":        "01964a3b-0000-7000-8000-ac6000000a01",
    "beneficiary_id":    "01964a3b-5e6f-7000-8000-beef00000001",
    "amount":            "1500.00",
    "payment_reference": "INV000042",
})
```

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

let draft = client.transfer_drafts().create(&serde_json::json!({
    "account_id": "01964a3b-0000-7000-8000-ac6000000a01",
    "beneficiary_id": "01964a3b-5e6f-7000-8000-beef00000001",
    "amount": "1500.00",
    "payment_reference": "INV000042",
}))?;
```

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

{:ok, draft} =
  Manza.TransferDrafts.create(client, %{
    "account_id" => "01964a3b-0000-7000-8000-ac6000000a01",
    "beneficiary_id" => "01964a3b-5e6f-7000-8000-beef00000001",
    "amount" => "1500.00",
    "payment_reference" => "INV000042"
  })
```

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

draft = client.transfer_drafts.create(
  account_id: "01964a3b-0000-7000-8000-ac6000000a01",
  beneficiary_id: "01964a3b-5e6f-7000-8000-beef00000001",
  amount: "1500.00",
  payment_reference: "INV000042"
)
```

```php
use Manza\Client;

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

$draft = $client->transferDrafts->create([
    'account_id' => '01964a3b-0000-7000-8000-ac6000000a01',
    'beneficiary_id' => '01964a3b-5e6f-7000-8000-beef00000001',
    'amount' => '1500.00',
    'payment_reference' => 'INV000042',
]);
```

```json
{
  "id": "01964a3b-1a2b-7000-8000-feed00000042",
  "status": "requested",
  "amount": "1500.00",
  "currency_code": "MAD",
  "payment_reference": "INV000042",
  "client_reference": null,
  "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,
  "authorization": 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.

`authorization` is the latest machine-authorization challenge, with its `id`, `status` and `expires_at`, or null when the draft never had one. `client_reference` is the id you sent on create.

cURL

CLI

JavaScript

Ruby

Python

Go

Rust

Elixir

Crystal

PHP

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

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

```javascript
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.transferDrafts.get("01964a3b-1a2b-7000-8000-feed00000042");
```

```ruby
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.transfer_drafts.get("01964a3b-1a2b-7000-8000-feed00000042")
```

```python
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.transfer_drafts.get("01964a3b-1a2b-7000-8000-feed00000042")
```

```go
import manza "github.com/getmanza/manza-go"

client, _ := manza.New(manza.WithAPIKey(os.Getenv("MANZA_API_KEY")))

draft, err := client.TransferDrafts.Get(ctx, "01964a3b-1a2b-7000-8000-feed00000042")
```

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

let draft = client.transfer_drafts().get("01964a3b-1a2b-7000-8000-feed00000042")?;
```

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

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

```crystal
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

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

```php
use Manza\Client;

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

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

```json
{
  "id": "01964a3b-1a2b-7000-8000-feed00000042",
  "status": "completed",
  "amount": "1500.00",
  "currency_code": "MAD",
  "payment_reference": "INV000042",
  "client_reference": null,
  "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"
  },
  "authorization": null,
  "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` | — |
| client_reference already used | 409 | `duplicate_client_reference` | `client_reference` |
| 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` | — |