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#
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 |
| 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:
{
"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 "https://ma.manza.dev/api/beneficiaries" \
-H "Authorization: Bearer sk_live_..."manza beneficiaries listimport { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });
await manza.beneficiaries.list();require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])
manza.beneficiaries.listfrom manza import Manza
client = Manza() # reads MANZA_API_KEY
client.beneficiaries.list()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{})let client = manza::Client::builder()
.api_key(std::env::var("MANZA_API_KEY")?)
.build()?;
let page = client.beneficiaries().list(Default::default())?;{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))
{:ok, page} = Manza.Beneficiaries.list(client)require "manza"
client = Manza::Client.new # reads MANZA_API_KEY
page = client.beneficiaries.listuse Manza\Client;
$client = new Client(apiKey: getenv('MANZA_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": "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.
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. These creation endpoints do not provide request idempotency; inspect saved recipients after a timeout before resubmitting using the read endpoints below.
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.
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"
}'manza transfers create --account-id 01964a3b-0000-7000-8000-ac6000000a01 \
--beneficiary-id 01964a3b-5e6f-7000-8000-beef00000001 --amount 1500.00 --payment-reference "INV000042"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",
});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"
)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",
)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",
})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",
}))?;{: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"
})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"
)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',
]);{
"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 "https://ma.manza.dev/api/payments/01964a3b-1a2b-7000-8000-feed00000042" \
-H "Authorization: Bearer sk_live_..."manza transfers get 01964a3b-1a2b-7000-8000-feed00000042import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });
await manza.transferDrafts.get("01964a3b-1a2b-7000-8000-feed00000042");require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])
manza.transfer_drafts.get("01964a3b-1a2b-7000-8000-feed00000042")from manza import Manza
client = Manza() # reads MANZA_API_KEY
client.transfer_drafts.get("01964a3b-1a2b-7000-8000-feed00000042")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")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")?;{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))
{:ok, draft} = Manza.TransferDrafts.get(client, "01964a3b-1a2b-7000-8000-feed00000042")require "manza"
client = Manza::Client.new # reads MANZA_API_KEY
draft = client.transfer_drafts.get("01964a3b-1a2b-7000-8000-feed00000042")use Manza\Client;
$client = new Client(apiKey: getenv('MANZA_API_KEY'));
$draft = $client->transferDrafts->get('01964a3b-1a2b-7000-8000-feed00000042');{
"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 | — |