GuidesSend a transfer

Send a transfer

Create an outgoing transfer with idempotency, understand its states (reserve, awaiting_approval, confirmed), and cancel a stuck request.

A transfer isn't broadcast to the network right away: the amount is reserved first, then the Policy Engine decides allow / queue / block, and only after that do the MPC nodes sign the transaction. This guide shows how to send a transfer, what happens to its state, and how to cancel a stuck request. For how the reserve works, see Ledger; for decisions, see Policy Engine.

The full path of a transfer, from creation to execution or return:

Prerequisites

Step 1. Send the transfer

POST /api/v1/vaults/{code}/transfers creates an outgoing transfer. Always pass an idempotencyKey: retrying with the same key returns the transfer that was already created instead of duplicating the payment.

Before creating a transfer, you can ask the Policy Engine whether it will go through by calling evaluate (allow/queue/block with no side effects). See Evaluate a transfer.

curl -X POST "[BASE_URL]/api/v1/vaults/acme-otc/transfers" \
  -H "Authorization: Bearer $V3_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "fromAddressId": "cmr3f41z20001psp7phmyapw3",
    "toAddress": "TQ2m...p4rX",
    "token": "usdt",
    "amount": "1350.00",
    "idempotencyKey": "payout-2026-03-14-001"
  }'

Under the hood, the platform resolves the address and token, converts the amount using decimals, checks the balance, and runs the request through the Policy Engine (action=transfer). If the transfer is forbidden, you get 403; if funds are insufficient, you get an INSUFFICIENT_BALANCE error.

{
  "transaction": { "...": "..." },
  "fromAddressId": "cmr3f41z20001psp7phmyapw3",
  "toAddress": "TQ2m...p4rX",
  "amount": "1350000000",
  "amountDecimal": "1350.00"
}

The amount is returned in two forms: amount in the smallest units (raw) and amountDecimal in human-readable format.

The request body fields shown here are for illustration only. Check the exact request schema on the create transfer page in the API Reference.

Step 2. The three transfer states

The outcome depends on the Policy Engine:

OutcomeTransfer statusReserve
allowexecuted (in demo → confirmed)debited
queueawaiting_approvalheld in reserved
blocknot created (403)not reserved

If the transfer is rejected or cancelled, the reserved amount returns to available.

Step 3. Check the status

curl "[BASE_URL]/api/v1/vaults/acme-otc/transfers/TRANSFER_ID" \
  -H "Authorization: Bearer $V3_TOKEN"

GET /api/v1/vaults/{code}/transfers/{id} returns the status, the amounts (raw and decimal), and, for transfers that require approval, a snapshot of the approvers and their votes.

Step 4. Approve (if the transfer is queued)

If the transfer is in awaiting_approval, members of the approver group vote:

curl -X POST "[BASE_URL]/api/v1/vaults/acme-otc/transfers/TRANSFER_ID/approvals" \
  -H "Authorization: Bearer $V3_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "decision": "approve" }'
  • approve counts toward the threshold; once it's reached, the transfer is executed.
  • reject is a veto: the transfer immediately becomes rejected, and the funds are returned.
  • The initiator can't vote on their own transfer; everyone gets one vote.

For more on approval requests, see Approvals.

Check the exact name of the vote field (decision here) on the transfer voting page in the API Reference.

Step 5. Cancel a stuck transfer

If approvers don't act, the initiator can withdraw the transfer while it's awaiting approval:

curl -X POST "[BASE_URL]/api/v1/vaults/acme-otc/transfers/TRANSFER_ID/cancel" \
  -H "Authorization: Bearer $V3_TOKEN"

cancel moves the request to rejected and returns the reserved funds. A transfer can't be cancelled once it's been approved or executed.

Common mistakes

SymptomCause
403 on creationthe transfer was blocked by a policy; check reason / trace
INSUFFICIENT_BALANCEnot enough available balance on the address
token doesn't resolvethe token doesn't match the address's network
cancel doesn't workthe transfer has already been approved or executed and can't be cancelled
payment was duplicatedno stable idempotencyKey was passed

You're done when

  • with allow, the transfer is confirmed (in demo);
  • with queue, an awaiting_approval request is created and the amount is in reserved;
  • you can read the status via GET /api/v1/vaults/{code}/transfers/{id}.

Next steps