Payroll
Draft a pay run; recipients add their own destination afterwards. Nothing is funded or paid until you say so, explicitly, twice.
Concept
A pay run holds a list of items — one per recipient, each with an amount. Recipients you haven't paid before get their own hosted link to add a bank account or wallet address; you never collect or transmit their destination on their behalf.
Resources
| Object | What it is |
|---|---|
payroll_runs | The run itself — status, funding state, currency, recipient summary. |
payroll_run_items | One line per recipient — destination status, amount, per-item status. |
payroll_recipients | People you've paid before. The one write: save a destination the recipient submitted as their default. |
payroll_templates | A saved item list from a previous run, for building the next one faster. |
payroll_recipient_sessions | The recipient's own hosted page (public token) to add or confirm their destination. |
Lifecycle
reviewed → funding_pending → funded → executing → completed, with partial, failed and cancelled as terminal branches. Read funding_state and the confirmed amount and currency alongside the run status. The classifier returns unfunded, partially_funded, funded or overfunded. Only the last two pass the separate sufficiency check; execute also rechecks the amount against the recorded requirement in the same currency. A status label alone is not funding proof.
Minimal flow
POST /v1/payroll_runs— draft it with recipients and amounts. Drafting is allowed before Bridge verification and moves no money; approval, funding and execution have separate eligibility checks.GET /v1/payroll_runs/{id}/readiness— a preflight over the payer-compliance gateexecutechecks first (Bridge verification, KYC, ToS, rail endorsement), without executing anything. Aready: trueanswer does not by itself guaranteeexecutesucceeds — funding, destinations and rail resolution are checked separately, at the mutation.POST .../approve, then fund it (bank or crypto funding instructions). Funding is detected by a poll every five minutes, not a webhook — on the crypto route each poll reads the wallet up to a minute before it began, so a real transfer can take about six minutes to show in the run'sstatusandfunding_state; the instructions' ownstatusstaysverifiedonce money is observed. Crypto deposit details issued more than 14 days ago on a run still waiting for funding readblockedwithfailure_reason: deposit_window_expiredand no deposit details: a deposit to them is no longer counted automatically — contact support. Bank funding in BRL (Pix) or MXN (SPEI) opens only once Bridge has approved those deposits on the employer's account: until then the instructions readblockedwithfailure_reasonfunding_endorsement_required(finish the verification step, then retry),funding_endorsement_in_review(Bridge is reviewing it) orfunding_rail_not_available(Bridge does not offer that deposit rail to this account; not retryable). A BRL account's Pix pay code isbank.br_code.POST .../execute— REST-only, the money boundary. This is the one step no MCP tool performs.GET /v1/payroll_runs/{id}or the MCP toolget_payroll_runto watch it complete.
Amount caps and row limit
The default caps are 25,000 for the run’s funding total (including any enabled fee) and 10,000 for each recipient amount, in the run’s own currency. They are configurable server limits, not USD equivalents: no currency conversion is applied by the check. Amount caps are checked at create and again before execute. Creation allows at most 500 rows; the execute amount-cap check does not repeat that row-count check.
When a recipient saves a destination
A destination the recipient saves through their own link makes that run's row ready; it does not pay it. Funding and explicit execution are still required. It does not become their default on its own: it waits on the recipient as pending_destination (masked — asset, network or rail, country, last four), and new runs keep asking until you save it.
GET /v1/payroll_recipients— a recipient with a non-nullpending_destinationhas answered. Show the masked destination.POST /v1/payroll_recipients/{id}/adopt_pending_destinationwith{"expected_submitted_at": "<pending_destination.submitted_at>"}. It saves the destination as the default and fills this recipient's rows that still wait for a destination in runs not yet approved —updated_run_itemslists them. A row the destination cannot pay (another currency) keeps asking the recipient and is listed inskipped_run_items.409 pending_destination_changed— the recipient saved another destination after you read it. Their default is unchanged: read the recipient again and show the new one before you save it. In a rare race — the recipient saves a new destination while your call runs — rows your call already filled with the destination you confirmed keep it; the next read of those runs shows them.
The recipient's own session reads back what they saved as destination — masked: kind, rail (or network) and last4, never the number. On a USD run a bank destination is a US bank account (ACH): bank.routing_number (its ABA check digit is verified), bank.account_number (4 to 17 digits), bank.bank_account_type (checking or savings) and the holder's US mailing address with state; bank_name is optional there. capabilities.bank.enabled is true only while such a payout can run (reason: bridge_bank_rail_disabled, bank_currency_unsupported). When the provider refuses the details the answer is 422 beneficiary_invalid, with param naming the field when it names exactly one.
Moves no money. A Swaps Wallet address on a network other than Tempo, which nothing can sign for, is refused 422 destination_rail_keyless before anything is written; on Tempo a Swaps Wallet is refused too when it cannot take new money — 422 destination_wallet_disabled, destination_wallet_key_reported_lost, destination_wallet_signer_at_risk or destination_wallet_signer_unconfirmed — ask the recipient for another destination. The same codes answer the recipient's own destination write, approve and execute (a run is refused whole for any row that pays such a wallet). When the check itself could not run, every one of them answers 503 destination_risk_unverifiable instead: nothing was written, and — except on execute, which records the block first — the same Idempotency-Key retries after Retry-After. The exact codes go only to the wallet's own holder; for anyone else's Swaps Wallet the answer is 422 destination_not_accepted, and every caller gets 503 destination_check_unavailable when the wallet or key-mark lookup itself failed. There is no MCP tool for this step: it changes where future payouts go, so a person confirms it.
A USD run paid out in local money (where enabled)
Where your account is enabled for it, a recipient of a USD pay run may be paid to a local bank account instead: Brazil by Pix (BRL), Mexico by SPEI (MXN) or Colombia by Bre-B (COP), each enabled separately. The run is still in USD and funded in USD or USDC; the provider converts each such payout once, when it sends it, at its own rate.
- The session lists what is offered in
capabilities.bank.local_currencies— each with a labelledfx_estimate(the item at the provider's rate, rounded down) and the provider'sminimumfor the rail. The recipient sendsbank.pix_key,bank.clabeorbank.bre_b_keywithbank.account_holder; no bank name or address. - An item below the provider's minimum at today's rate answers
409 local_payout_below_minimum— when it is saved, atapproveand again atexecute— before anything is created, provisioned or queued. PayrollRunItem.local_payoutshows the estimate before the payout and exactly what the provider's receipt says wasdeliveredafter. The item'samountand the 1% fee stay in USD; the provider's exchange spread is inside its rate, never a separate line.
Pay runs not in USD
By default the payroll funding wallet holds USDC (the run's pool_asset). A run in another currency (EUR today) is funded into that wallet through its own virtual account, so the money that pays the run is the USDC the deposit converted into, at the rate the provider realized on it. Two consequences, both visible on the wire:
- A USDC wallet destination is paid the item amount converted at that realized rate, rounded down to two decimal places — the precision the provider accepts on a transfer — and carried on the wire as a 6-decimal
Money; the 1% fee, when enabled, is converted the same way and netted on top. Once the run is funded, the recipient's session states the exact figure (capabilities.crypto_external_wallet.fx_estimate.kind: realized); before funding it carries a labelled provider estimate (kind: estimate). Before the first transfer the exact body is rehearsed with the provider (a dry run that creates nothing).executerefuses withfunding_realized_rate_unavailable,funding_pool_insufficient,funding_pool_asset_unknownorpayout_preflight_rejectedrather than pay a number it cannot stand behind; in every case nothing is queued and the run stays funded. - A bank destination is not available for such a run: paying EUR out of a USDC pool would mean a second conversion the run never priced. The session reads
capabilities.bank.enabled: falsewithreason: bank_payout_fx_unsupported, andpayroll_recipient_sessions.destination.setrefuses a bank account with the same code. A USD run is unaffected.
EUR runs paid in EURC (where enabled)
Where your account is enabled for an EURC pool, a recipient of an EUR pay run may choose an EURC wallet on Ethereum or Base instead of a USDC wallet: the session lists it in capabilities.crypto_external_wallet.assets, and the recipient sends crypto.asset: "EURC" with the destination. The run is then funded into an EURC pool instead of USDC:
- One pool per run.
approvechooses it from the recipients' wallets and publishes it as the run'spool_asset; a bank destination casts no vote and keeps its own blocker. A run that mixes EURC and USDC wallets answers409 run_mixed_pool—details.items_by_poolcounts the wallet rows asking for each — and nothing is approved: split it into two runs. An EURC pool is funded by bank transfer only: acryptorun answers409 crypto_funding_pool_unsupported. Once money has been observed on the run its pool never changes (409 pool_asset_locked). - Its own IBAN. The run's SEPA funding instructions credit EURC; they are never the instructions of a USDC pool.
- Paid at the rate the deposit realized. Each EURC wallet receives its amount times that rate (EURC credited ÷ EUR deposited — expected one for one, never assumed), rounded down to two decimal places, with the same pool guard, provider dry run and refusals as a USDC wallet of an EUR run. Before the run is funded the session states no EURC figure; after,
fx_estimate.kind: realized. - Ethereum and Base only. An EURC wallet on Polygon or Arbitrum is refused
409 asset_network_unsupportedwhen it is saved, before anything is written — never at payout. - No bank payout yet. A bank destination is still not available for such a run (
bank_payout_fx_unsupported).
Elsewhere crypto.asset is USDC (also when omitted), and an EURC wallet the run is not offered is refused like any other asset it does not pay (400 invalid_request).
What an agent can and cannot do here
prepare_payroll_run drafts a run for review — it never collects bank or wallet details itself, and it never funds or executes anything. execute has no MCP tool; running payroll is deliberately a REST-only, human-confirmed action, the same boundary payment link activation draws.
Next: the reference for the full run and item schemas · Conventions for the fee fields on the wire. When enabled, the fee is 1% of each recipient amount, added on top and rounded per item: an amount of 100 requires 101 in funding. Read the server-provided fee split; do not apply 1% to the gross funding total.