Beetles Docs

Transfers

Move money between accounts — single, split, or batch.

All transfers go through a single endpoint:

POST /v1/transfers

The request body determines which mode is used.


Single Transfer

Move funds from one account to another.

{
  "ledger_id": "...",
  "debit_account_id": "...",
  "credit_account_id": "...",
  "amount": 10000,
  "code": 1
}

Returns 201 Created with the transfer object.

Using Labels Instead of IDs

You can reference ledgers and accounts by their human-readable label instead of UUIDs. Labels are case-insensitive and mutually exclusive with their _id counterparts.

{
  "ledger_label": "main-ledger",
  "debit_account_label": "revenue",
  "credit_account_label": "expenses",
  "amount": 10000,
  "code": 1
}

You cannot send both ledger_id and ledger_label in the same request — pick one. Same for account fields.

Two-Phase (Pending)

Add pending: true to create a hold. Optionally set timeout (seconds) or settlement_date.

{
  "ledger_id": "...",
  "debit_account_id": "...",
  "credit_account_id": "...",
  "amount": 10000,
  "code": 1,
  "pending": true,
  "timeout": 3600
}

Then confirm or void:

POST /v1/transfers/{id}/confirm
POST /v1/transfers/{id}/void

See Settlement for details on settlement cycles and scheduled dates.


Split Transfer

Distribute funds from one source to multiple destinations in a single request.

{
  "ledger_id": "...",
  "debit_account_id": "...",
  "amount": 10000,
  "code": 1,
  "split": [
    { "account_id": "acct-merchant", "amount": 9500 },
    { "account_id": "acct-platform-fee", "amount": 500 }
  ]
}

Split amounts must sum exactly to the transfer amount.

How It Works

Under the hood, the API constructs linked transfer pairs through a control account. All legs succeed or all fail atomically.

Property Inheritance

When pending, timeout, settlement_date, or code is set on a split transfer, every leg inherits those properties automatically.

{
  "ledger_id": "...",
  "debit_account_id": "...",
  "amount": 10000,
  "code": 1,
  "settlement_date": "2025-03-01",
  "split": [
    { "account_id": "acct-merchant", "amount": 9500 },
    { "account_id": "acct-platform-fee", "amount": 500 }
  ]
}

Each leg becomes a pending transfer settled on March 1st. You can confirm or void each leg independently.


Batch Transfer

Send multiple transfers in a single request. Processed asynchronously.

{
  "transfers": [
    {
      "ledger_id": "...",
      "debit_account_id": "...",
      "credit_account_id": "...",
      "amount": 50000,
      "code": 1,
      "timestamp": "2024-01-15T10:00:00-03:00",
      "metadata": { "source": "legacy", "original_id": "TXN-001" }
    },
    {
      "ledger_id": "...",
      "debit_account_id": "...",
      "credit_account_id": "...",
      "amount": 15000,
      "code": 2,
      "timestamp": "2024-01-15T10:05:00-03:00"
    }
  ]
}

Returns 202 Accepted with a workflow_id for tracking.

Labels in Batch Mode

Batch entries also support ledger_label, debit_account_label, and credit_account_label:

{
  "transfers": [
    {
      "ledger_label": "main-ledger",
      "debit_account_label": "funding",
      "credit_account_label": "revenue",
      "amount": 50000,
      "code": 1,
      "timestamp": "2024-01-15T10:00:00-03:00"
    }
  ]
}

Rules

  • Timestamps must be RFC 3339 with timezone offset.
  • Maximum 1,000 transfers per request.
  • Each transfer is written atomically to both the ledger engine and the database.
  • The timestamp is stored as created_at for ordering — the ledger engine assigns its own internal timestamps. This is the same behavior as timestamp on single transfers.

Batch transfers cannot use pending, split, or settlement_date. Use separate single or split transfers for those features.


Auto-Create Mode

When importing data from external systems, you may not want to pre-create every ledger and account. Use the top-level auto-create flags to let the API provision them on the fly:

FlagEffect
allow_ledger_createIf a ledger_label doesn't exist, auto-creates the ledger (with an associated @world account).
allow_accounts_createIf an account label doesn't exist, auto-creates the account in the resolved ledger.
fund_from_worldEnables overdraft on auto-created debit accounts so @world can fund them.
{
  "allow_ledger_create": true,
  "allow_accounts_create": true,
  "fund_from_world": true,
  "transfers": [
    {
      "ledger_label": "usd-treasury",
      "debit_account_label": "funding",
      "credit_account_label": "revenue",
      "amount": 50000,
      "code": 1,
      "timestamp": "2024-01-15T10:00:00-03:00"
    }
  ]
}

The API dispatches provisioning workflows for any missing resources. The transfer workflow waits for all provisioning to complete (via signals) before executing the transfers. Returns 202 Accepted with a workflow_id.

Auto-create flags are top-level and apply to all entries in the request. They work with single, split, and batch modes.


Idempotency

Include an idempotency_key (string) in your JSON payload to prevent duplicate transfers on retries. The key is scoped per workspace and resource type, and never expires.

{
  "ledger_label": "main",
  "debit_account_label": "revenue",
  "credit_account_label": "expenses",
  "amount": 10000,
  "code": 1,
  "idempotency_key": "user-signup-bonus-12345"
}

Reference

Every transfer can include a reference field (must be a valid UUID) for external correlation, and a metadata object for arbitrary key-value pairs.

On this page