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 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) 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.

Required scopes#

EndpointScope
Initiate a transfertransfers:write
Get transfer drafttransfers:read
Authorize / decline a transfer (separate key)transfers:authorize
List / Get beneficiaries and bank accountsbeneficiaries:read
Create beneficiaries / bank accountsbeneficiaries:write
Request trust for payeesbeneficiaries: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:

payment.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.

request.sh
curl "https://ma.manza.dev/api/beneficiaries" \
  -H "Authorization: Bearer sk_live_..."
manza
manza beneficiaries list
app.js
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.beneficiaries.list();
app.rb
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.beneficiaries.list
app.py
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.beneficiaries.list()
main.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{})
main.rs
let client = manza::Client::builder()
    .api_key(std::env::var("MANZA_API_KEY")?)
    .build()?;

let page = client.beneficiaries().list(Default::default())?;
app.exs
{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))

{:ok, page} = Manza.Beneficiaries.list(client)
app.cr
require "manza"

client = Manza::Client.new # reads MANZA_API_KEY

page = client.beneficiaries.list
app.php
use Manza\Client;

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

$page = $client->beneficiaries->list();
response.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.

create-beneficiary.sh
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.

add-bank-account.sh
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.

request.sh
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
manza transfers create --account-id 01964a3b-0000-7000-8000-ac6000000a01 \
  --beneficiary-id 01964a3b-5e6f-7000-8000-beef00000001 --amount 1500.00 --payment-reference "INV000042"
app.js
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",
});
app.rb
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"
)
app.py
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",
)
main.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",
})
main.rs
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",
}))?;
app.exs
{: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"
  })
app.cr
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"
)
app.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',
]);
response.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.

request.sh
curl "https://ma.manza.dev/api/payments/01964a3b-1a2b-7000-8000-feed00000042" \
  -H "Authorization: Bearer sk_live_..."
manza
manza transfers get 01964a3b-1a2b-7000-8000-feed00000042
app.js
import { Manza } from "@getmanza/sdk";
const manza = new Manza({ apiKey: process.env.MANZA_API_KEY });

await manza.transferDrafts.get("01964a3b-1a2b-7000-8000-feed00000042");
app.rb
require "manza"
manza = Manza.new(api_key: ENV["MANZA_API_KEY"])

manza.transfer_drafts.get("01964a3b-1a2b-7000-8000-feed00000042")
app.py
from manza import Manza
client = Manza()  # reads MANZA_API_KEY

client.transfer_drafts.get("01964a3b-1a2b-7000-8000-feed00000042")
main.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")
main.rs
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")?;
app.exs
{:ok, client} = Manza.new(api_key: System.fetch_env!("MANZA_API_KEY"))

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

client = Manza::Client.new # reads MANZA_API_KEY

draft = client.transfer_drafts.get("01964a3b-1a2b-7000-8000-feed00000042")
app.php
use Manza\Client;

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

$draft = $client->transferDrafts->get('01964a3b-1a2b-7000-8000-feed00000042');
response.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#

ScenarioStatusTypeParam
Unknown account / beneficiary / destination422validation_erroraccount_id
Both or neither recipient field given422validation_errorbeneficiary_id
Amount ≤ 0 or not a number422validation_erroramount
Amount exceeds available balance422insufficient_funds—
client_reference already used409duplicate_client_referenceclient_reference
Key's creator may not create transfer drafts403forbidden_error—
API transfers not enabled403forbidden_error—
Initiation rate limit exceeded429rate_limit_error—