# Payroll — Swaps API

Pay people — runs, items, recipients, templates.

**Jump to an operation:**

- [GET /payroll_runs](#list-pay-runs-available)
- [POST /payroll_runs](#draft-a-pay-run-available)
- [GET /payroll_runs/{id}](#get-a-pay-run-available)
- [POST /payroll_runs/{id}/approve](#approve-a-reviewed-run-available)
- [POST /payroll_runs/{id}/cancel](#cancel-a-pay-run-available)
- [POST /payroll_runs/{id}/execute](#execute-a-funded-run-available)
- [GET /payroll_runs/{id}/readiness](#preview-the-payer-compliance-gate-available)
- [GET /payroll_runs/{id}/funding_instructions](#get-funding-instructions-available)
- [POST /payroll_runs/{id}/funding_instructions](#create-or-refresh-funding-instructions-available)
- [GET /payroll_runs/{id}/items](#list-a-runs-items-available)
- [GET /payroll_runs/{id}/items/{item_id}](#get-one-run-item-available)
- [POST /payroll_runs/{id}/items/{item_id}/reissue_link](#rotate-and-re-send-a-recipients-link-available)
- [GET /payroll_runs/{id}/attempts](#list-a-runs-payout-attempts-available)
- [GET /payroll_runs/{id}/events](#list-a-runs-events-available)
- [GET /payroll_runs/{id}/export](#export-a-run-as-csv-available)
- [GET /payroll_recipients](#list-recipients-available)
- [GET /payroll_recipients/{id}](#get-a-recipient-available)
- [POST /payroll_recipients/{id}/adopt_pending_destination](#save-a-recipients-submitted-destination-as-their-default-available)
- [GET /payroll_templates](#list-templates-available)
- [POST /payroll_templates](#save-a-run-as-a-template-available)
- [GET /payroll_templates/{id}](#get-a-template-available)
- [GET /payroll_recipient_sessions/{token}](#get-a-recipients-own-session-available)
- [POST /payroll_recipient_sessions/{token}/destination](#set-a-destination-available)

> Generated from the bundled OpenAPI contract. The HTML page and this Markdown twin share the same processed operation descriptions and status annotations.

<a id="list-pay-runs-available"></a>
## `GET /payroll_runs` — List pay runs — available

**Operation ID:** `payroll_runs.list`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

List this employer's pay runs, newest first. `status_group` mirrors the Today inbox
grouping rather than the raw 8-status enum, so a caller does not have to re-derive it.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |
| `status_group` | query | no | `needs_you` \| `in_progress` \| `done` | This resource's own bucket set (`needs_you`, `in_progress`, `done`), shared with payouts and orders only — not a universal enum. `needs_you` = reviewed, funding_pending; `in_progress` = funded, executing; `done` = completed, partial, failed, cancelled. `draft` and `approved` (BL-15, G decision 2026-09-14) match NONE of these three buckets — a run in either status is published on the unfiltered list but is invisible to every `status_group`-filtered read, including `needs_you` even though `approved` genuinely is money-pending. Known gap, not yet bucketed (a G decision, since it changes tab counts). |
| `expand` | query | no | `string` | Comma-separated. `summary` (LIST-SUMMARY-1) adds `summary`: counts computed server-side over EVERY row matching this request's filters, not over the returned page, so a client never adds up a paginated page and calls it a total. `limit`/`cursor` never change it. An unrecognised value is ignored rather than rejected (the `GET /account` `expand` rule). |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of pay runs. | `application/json` [PayrollRunList](/reference/~schemas.md#payrollrunlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="draft-a-pay-run-available"></a>
## `POST /payroll_runs` — Draft a pay run — available

**Operation ID:** `payroll_runs.create`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.write` · **Test mode:** unavailable

Create a pay run with its recipients and amounts. This is a **draft only** — nothing is
funded and nothing is paid. `runs_create` writes `reviewed` directly; there is no `draft`
state on `/v1`. Default amount caps are 25,000 on the funding total (including any enabled
fee) and 10,000 per recipient amount, in the run currency without FX conversion.
Exceeding either configured amount cap answers `422`; amount caps are rechecked at
`execute`. The 500-row limit is create request validation (`400`), not a repeated
execute check. `funding_method: crypto` drafts a run
the employer funds by sending USDC to their Bridge payroll wallet — the same path the
dashboard uses. `create` refuses it with `409 capability_unavailable` only while crypto
funding is switched off (`crypto_funding.reason: crypto_funding_not_enabled`) and never
substitutes fiat. For the other reasons (`bridge_funding_currency_unsupported`,
`verification_required`) the run is created but cannot be funded: `approve` refuses it or
its funding instructions read `blocked` — check `crypto_funding.available` first.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [PayrollRunCreateRequest](/reference/~schemas.md#payrollruncreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The created run. | `application/json` [PayrollRun](/reference/~schemas.md#payrollrun) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-a-pay-run-available"></a>
## `GET /payroll_runs/{id}` — Get a pay run — available

**Operation ID:** `payroll_runs.get`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

Return one pay run this caller owns.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The run. | `application/json` [PayrollRun](/reference/~schemas.md#payrollrun) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="approve-a-reviewed-run-available"></a>
## `POST /payroll_runs/{id}/approve` — Approve a reviewed run — available

**Operation ID:** `payroll_runs.approve`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.write` · **Test mode:** unavailable

Approve a `reviewed` run. `runs_approve` writes `funding_pending` directly — there is no
published `approved` state to observe afterwards. Refused when the readiness ladder is not
clear; read `GET .../readiness` first to show why, rather than discovering it from this
call's error. A run with zero rows answers `409 run_has_no_rows` before any provider call
or funding-account provisioning. Approving prepares the run's funding instructions for its
own `funding_method`; a `crypto` run answers `409 capability_unavailable` while crypto
funding is switched off, before anything is provisioned — it is never approved onto a bank
route silently.

Approving chooses the run's funding pool (`pool_asset`) from its recipients' wallet
destinations: `USDC`, or `EURC` for an EUR run whose recipients chose EURC wallets where the
EURC pool is enabled for the account; a bank account casts no vote and keeps its own
blocker. One pool per run, decided before any provider call and any write: a run that mixes
EURC and USDC wallets answers `409 run_mixed_pool` (`details.items_by_pool` counts the wallet
rows per asset; split it into two runs), and an EURC pool on a `crypto` run answers `409
crypto_funding_pool_unsupported` (an EURC pool is funded by bank transfer only).

A USD run whose recipients saved local bank accounts (BRL by Pix, MXN by SPEI, COP by Bre-B —
`PayrollRunItem.local_payout`) has each such row priced at Bridge's rate before anything is
provisioned: a row below Bridge's minimum for its rail answers `409 local_payout_below_minimum`
(`details`: `item_id`, `basis` — `destination` for the rail's minimum in the local currency,
`route_source` for Bridge's USDC minimum — `minimum`, `minimum_currency`), and an unreadable rate
`503 temporarily_unavailable`; nothing is written either way. A row whose rail is not enabled for
the payer leaves the funding instructions `blocked` with `failure_reason: local_payout_not_enabled`.

Every row's crypto destination is checked again, before any provider call and any write, as every
inbound door checks a Swaps Wallet: a row that pays one Swaps cannot route new money to refuses the
whole run with that row's code — `422 destination_rail_keyless`, `destination_wallet_disabled`,
`destination_wallet_key_reported_lost`, `destination_wallet_signer_at_risk` or
`destination_wallet_signer_unconfirmed` (the first of these, in this order, that the rows carry; cancel
the run and create a new one, which asks the recipient again) — and `503 destination_risk_unverifiable`
when the check could not run for a row and no row is refused outright: nothing was written, retry after
`Retry-After` with the same `Idempotency-Key`.
Those codes go only to the wallet's own holder; an employer never is one, so it hears
`422 destination_not_accepted` for any of them, and every caller hears
`503 destination_check_unavailable` when the wallet or key-mark lookup itself failed.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The approved run. | `application/json` [PayrollRun](/reference/~schemas.md#payrollrun) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="cancel-a-pay-run-available"></a>
## `POST /payroll_runs/{id}/cancel` — Cancel a pay run — available

**Operation ID:** `payroll_runs.cancel`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.write` · **Test mode:** unavailable

Cancel a run that has not finished executing. Cancelling leaves any already-paid items
exactly where they were — there is no item-level `cancelled` status, and this never claws
back a payment already in flight.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The cancelled run. | `application/json` [PayrollRun](/reference/~schemas.md#payrollrun) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="execute-a-funded-run-available"></a>
## `POST /payroll_runs/{id}/execute` — Execute a funded run — available

**Operation ID:** `payroll_runs.execute`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.write` · **Test mode:** unavailable · **Money boundary:** per-transaction human confirmation required

Pay every ready item in one funded run. This is the money boundary — require a per-run
human confirmation before calling it, never chain it automatically from `approve` or from
a funding webhook. Execution is a sequential, unbatched server loop with no per-item push;
re-read the run or its items to watch progress, do not assume "N of M" from this response
alone. REST-only — structurally excluded from the MCP tool set because a generated tool
would let an agent cross the boundary without the human confirmation the flow requires.
A `funded` run only means an inbound funding event was observed — it does not mean the run
is fully funded; read `funding_state` and `funding_confirmed_amount` on the run before
confirming this call with a human.

Readiness refusals include `payroll_verified_customer_required` (the employer's Bridge
customer is not yet verified) and amount-cap errors `payroll_run_limit_exceeded` /
`payroll_item_limit_exceeded` (`422`). Default amount caps are 25,000 on the funding
total and 10,000 per recipient amount, in the run currency without FX conversion;
server configuration may change them. The 500-row limit is enforced at create, not
repeated by the execute amount-cap check. Other readiness refusals include
`funding_not_verified` (no confirmed funding event yet),
`payroll_run_underfunded` (an event was observed but the confirmed amount is short of
`funding_amount`, or was never confirmed — the message names the shortfall),
`recipient_destination_not_ready` (an item still needs a destination),
`rail_not_ready`, `provider_adapter_disabled`, `bridge_wallet_missing` and
`bridge_customer_missing`. None of these clear by retrying without changing the
underlying state — re-read `GET .../readiness` after acting on the cause.
A run with zero rows answers `409 run_has_no_rows` before any of these rungs and is
never moved to `executing`.

Every row's crypto destination is checked again before any payout attempt and any provider
call, as every inbound door checks a Swaps Wallet: a row that pays one Swaps cannot route new
money to blocks the whole run with that row's code — `422 destination_rail_keyless`,
`destination_wallet_disabled`, `destination_wallet_key_reported_lost`,
`destination_wallet_signer_at_risk` or `destination_wallet_signer_unconfirmed`; no payout is
started and Swaps support changes the destination — and `503 destination_risk_unverifiable`
when the check could not run for a row and no row is refused outright. Either way the block is
recorded on the run (`execution_blocked`), so this `503` carries no `Retry-After`: a replay of
the same `Idempotency-Key` answers `409 idempotency_failed`; read the run, then retry with a
new key.
Those codes go only to the wallet's own holder; an employer never is one, so it hears
`422 destination_not_accepted` for any of them, and every caller hears
`503 destination_check_unavailable` when the wallet or key-mark lookup itself failed.

A run not denominated in the asset its payout wallet holds (a EUR run funded into USDC)
pays its USDC wallet items from the converted pool at the rate the deposit actually
realized — each item converted and rounded down to the provider's two-decimal transfer
precision, the employer-funded fee converted the same way — and refuses, before any
payout, with `funding_realized_rate_unavailable` when that rate cannot be read from the
run's own funding evidence, `funding_pool_insufficient` when the converted payouts plus
fees would overdraw the pool, or `funding_pool_asset_unknown` when nothing states the
wallet's asset. Before its first transfer such a run is rehearsed with the provider at
the exact converted amounts (a `dry_run`, creating nothing): `payout_preflight_rejected`
refuses the run whole when the provider does not validate that body — its `details` carry
`preflight_reason` (`provider_refused`, `route_invalid`, or `dry_run_returned_transfer`
when the provider answered the dry run with a transfer object: the account is then on
operator hold, `403 payroll_operator_hold` on approve, fund and execute, until Swaps has
reconciled with the provider), `item_id`, and an allowlisted classification of the
provider's answer (`provider_status`, `provider_error_class`, `invalid_fields`,
`notes_count`, `notes_classes`), never its text — and a provider outage during the
rehearsal is `503 temporarily_unavailable` — in both cases nothing is queued and the run
stays funded. Bank items of such a run are `rail_not_ready`
(`bank_payout_fx_unsupported`). A USD run is unaffected.

An EUR run whose pool is `EURC` is settled the same way: its EURC wallet items are paid at
the rate its deposit realized (EURC credited ÷ EUR deposited — never assumed 1:1), rounded
down, from the pool and under the same guard, rehearsed with the provider before the first
transfer, with the same refusals; its bank items are `rail_not_ready`
(`bank_payout_fx_unsupported`).

A USD run's local bank rows (BRL by Pix, MXN by SPEI, COP by Bre-B) are paid from its USDC, 1:1,
and Bridge converts each once, into the account's currency at its own rate. Before the first
transfer each such row is priced again at Bridge's rate — `409 local_payout_below_minimum` (with
`details`) or `503 temporarily_unavailable` refuse the run whole, nothing queued, the run stays
funded — and its exact body is rehearsed with the provider as a `dry_run`, with the same
`payout_preflight_rejected` refusal as above. A row whose rail was disabled for the payer since
approval is `rail_not_ready` (`local_payout_not_enabled`). What Bridge delivered is recorded from its
receipt on the item (`PayrollRunItem.local_payout.delivered`).

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | `object` |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The run, now `executing` or further along. | `application/json` [PayrollRun](/reference/~schemas.md#payrollrun) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="preview-the-payer-compliance-gate-available"></a>
## `GET /payroll_runs/{id}/readiness` — Preview the payer-compliance gate — available

**Operation ID:** `payroll_runs.get_readiness`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

Preview the payer-compliance gate `approve`, funding and `execute` each check FIRST,
before their own state-specific checks — the same Bridge-customer verification, KYC, ToS
and rail-endorsement ladder (`PayrollBlockerCode`), without executing anything. It also
reports one run-state rung, `run_has_no_rows`, so a run with zero rows is never `ready`.
`ready: true` means THIS gate is clear; it is not a promise the mutation will succeed — a
run this reports ready can still be refused by a state check this preview does not run
(missing recipient destinations, unconfirmed funding, an unresolved rail, the private-beta
caps, the execution-adapter flag) and answer its own `409`/`422`/`503` with a curated code
from `execute`'s own error responses. A future, separate preview over that state machine is
not ruled out — it needs its own reason-code vocabulary, which `PayrollBlockerCode` does
not carry.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | Whether the run can proceed, and why not. | `application/json` [PayrollReadiness](/reference/~schemas.md#payrollreadiness) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-funding-instructions-available"></a>
## `GET /payroll_runs/{id}/funding_instructions` — Get funding instructions — available

**Operation ID:** `payroll_runs.get_funding_instructions`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

Read the run's current funding instructions and their status. The status describes the
instructions, not the money: it stays `verified` after a deposit is observed — read the
run's `status` and `funding_state` for whether it has been funded. Detection runs on a
poll, not a webhook: every five minutes, and on the crypto route each poll reads the
wallet only up to a minute before it began, so the run can show a real transfer about six
minutes late — do not treat an unchanged `funding_state` as proof nothing arrived. A crypto
run whose deposit details were issued more than 14 days ago reads `blocked` with
`failure_reason: deposit_window_expired` and no deposit details: a deposit to them is no
longer counted automatically — contact support.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The funding instructions. | `application/json` [PayrollFundingInstructions](/reference/~schemas.md#payrollfundinginstructions) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | The run's own funding instructions were never provisioned yet (`payroll_run_funding_instructions_not_ready` — `approve` provisions the first one; not the same as "no such run / not yours", which stays `404`). | `application/json` [Error](/reference/~schemas.md#error) |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="create-or-refresh-funding-instructions-available"></a>
## `POST /payroll_runs/{id}/funding_instructions` — Create or refresh funding instructions — available

**Operation ID:** `payroll_runs.fund`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.write` · **Test mode:** unavailable · **Money boundary:** per-transaction human confirmation required

Create the run's funding instructions on the first call; every later call is an idempotent
refresh of the same attempt's estimate, never a second provider transfer. This is the
money boundary — show it to a human and confirm before wiring or sending funds; never
chain it automatically from `create` or `approve`. `funding_method` in the body switches
the run between the bank route (`fiat`) and a USDC deposit to the employer's Bridge
payroll wallet (`crypto`); omitted, the run keeps its own method. A crypto run or override
answers `409 capability_unavailable` while crypto funding is switched off — it never falls
back to the bank shape silently. On a crypto run whose instructions are `verified` with an
amount, a call that does not switch method returns those same instructions unchanged: it
never re-prices the amount or restarts the deposit window — a run whose deposit details
were issued more than 14 days ago therefore keeps reading `blocked` with `failure_reason:
deposit_window_expired`. Switching method once money has
been observed on the run (`funding_state` other than `unfunded`, or a
`funding_confirmed_amount`) answers `409 funding_method_locked`. A run with zero rows
answers `409 run_has_no_rows` before any provider call. The run's funding pool
(`pool_asset`) is chosen again from its destinations, as at `approve` — `409 run_mixed_pool`,
and `409 crypto_funding_pool_unsupported` for a `crypto` method on an EURC pool; a pool that
would change once money has been observed answers `409 pool_asset_locked`. All three answer
before any provider call and any write.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **no**

| Media type | Schema |
| --- | --- |
| `application/json` | `object` |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The funding instructions, created or refreshed. | `application/json` [PayrollFundingInstructions](/reference/~schemas.md#payrollfundinginstructions) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | The requested funding method or route cannot serve this run — including `capability_unavailable` for `funding_method:'crypto'` while crypto funding is switched off, and `funding_method_locked` for a method switch after money was observed. Do not retry unchanged. | `application/json` [Error](/reference/~schemas.md#error) |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-a-runs-items-available"></a>
## `GET /payroll_runs/{id}/items` — List a run's items — available

**Operation ID:** `payroll_runs.items.list`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

List the per-recipient items on one run, in the run's own frozen row order (the order
recipients were added to the run, never re-sorted newest-first the way most other list
operations default). This is the run detail screen's own source for its frozen-rows
table — `payroll_runs.get` does not embed items — and that table's own row order must
match the run's, not an activity-feed order.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of run items. | `application/json` [PayrollRunItemList](/reference/~schemas.md#payrollrunitemlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-one-run-item-available"></a>
## `GET /payroll_runs/{id}/items/{item_id}` — Get one run item — available

**Operation ID:** `payroll_runs.items.get`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

Return one item on a run this caller owns.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `item_id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The item. | `application/json` [PayrollRunItem](/reference/~schemas.md#payrollrunitem) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="rotate-and-re-send-a-recipients-link-available"></a>
## `POST /payroll_runs/{id}/items/{item_id}/reissue_link` — Rotate and re-send a recipient's link — available

**Operation ID:** `payroll_runs.items.reissue_link`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.write` · **Test mode:** unavailable · **Money boundary:** per-transaction human confirmation required

The employer's way out of an expired, lost or never-delivered destination-request e-mail: rotates the
item's recipient token, re-sends the destination-request e-mail, and returns the fresh URL directly — the
only place the token reaches the wire, and only to this run's own employer (never a bare field, never in a
response to the recipient's own read of this item). Refused `409 conflict` once the item can no longer
accept a destination change (the same gate the recipient-facing destination write itself checks — reissuing
past it would mail a link that can never do anything), and rate-limited per item over a rolling window
shared with that same recipient-facing ceiling. A call inside the short idempotent reissue window right
after a genuine rotation returns the SAME still-usable link without rotating or mailing again —
`email.sent` is `false` for that call, never a claim that mail went out twice. `email.reason` names the
transport's own failure class (never the raw provider body) so a caller can show why nothing arrived
instead of a silent no-op.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `item_id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The rotated link and whether the re-send mailed. | `application/json` [PayrollRunItemLinkReissue](/reference/~schemas.md#payrollrunitemlinkreissue) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-a-runs-payout-attempts-available"></a>
## `GET /payroll_runs/{id}/attempts` — List a run's payout attempts — available

**Operation ID:** `payroll_runs.attempts.list`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

List the provider-side attempts behind a run's items, paginated. `handleRunsGet` already
returns every attempt embedded and unbounded — this is the paginated, independently
readable form of the same data.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of attempts. | `application/json` [PayrollAttemptList](/reference/~schemas.md#payrollattemptlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-a-runs-events-available"></a>
## `GET /payroll_runs/{id}/events` — List a run's events — available

**Operation ID:** `payroll_runs.events.list`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** sandbox

List one run's immutable event timeline, paginated. Same underlying data
`handleRunsGet` already returns unbounded — this is the paginated, independently
readable form. Test mode: sandbox (K11-6, §4.2 drift, was declared `full`) — a test run's events are written
by DEV handlers into DEV's own `api_events` outbox; a prod-local read would return an empty page forever.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of events. | `application/json` [PayrollEventList](/reference/~schemas.md#payrolleventlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="export-a-run-as-csv-available"></a>
## `GET /payroll_runs/{id}/export` — Export a run as CSV — available

**Operation ID:** `payroll_runs.export`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

Download the run as a CSV — recipients, amounts and masked destinations. Destinations stay
masked in the export exactly as they are on every other read; this is a report, not a
beneficiary-detail disclosure.

**Columns, in order:** `row_number`, `recipient_name`, `recipient_email`, `worker_type`,
`amount`, `currency`, `destination_type`, `destination_display`, `payout_status`,
`provider_id`, `rail_type`, `provider_reference`, `provider_transfer_id`, `failure_reason`,
`retry_eligible`, `paid_at`. `rail_type` carries the same provider-scoped rail identifier
as `PayrollAttempt.rail_type` — `bank_bridge`, `bridge_crypto_wallet` or `tempo_wallet`
today, an open set, and not a `Rail` value; like `provider_id` it names the provider. A
provider-neutral `rail_type` vocabulary is an A1-10 (naming freeze) follow-up. The header
row itself is part of the contract — do not parse this file positionally without checking it.

**Cell neutralisation.** Any cell whose first character is `=`, `+`, `-`, `@`, a tab or a
carriage return is emitted with a leading apostrophe (`'`). Those characters start a
formula in Excel, Sheets and Numbers, and the values here are supplied by third parties
(a recipient's own name or email), so an un-neutralised export would execute a stranger's
formula on the employer's machine. Quoting alone does not prevent this — a spreadsheet
unquotes the field first and evaluates it second. Strip the leading apostrophe if you are
parsing the file programmatically rather than opening it in a spreadsheet.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The CSV file. | `text/csv` `string` |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-recipients-available"></a>
## `GET /payroll_recipients` — List recipients — available

**Operation ID:** `payroll_recipients.list`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

List this employer's payees across every run. A recipient exists only as a byproduct of a
run's creation — there is no create, update or archive here, because nothing in the backend
writes to a recipient outside that path. The one write is `adopt_pending_destination`: a
recipient with a non-null `pending_destination` saved a destination through their payout
link that is not their default yet.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of recipients. | `application/json` [PayrollRecipientList](/reference/~schemas.md#payrollrecipientlist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-a-recipient-available"></a>
## `GET /payroll_recipients/{id}` — Get a recipient — available

**Operation ID:** `payroll_recipients.get`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

Return one recipient this caller owns.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The recipient. | `application/json` [PayrollRecipient](/reference/~schemas.md#payrollrecipient) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="save-a-recipients-submitted-destination-as-their-default-available"></a>
## `POST /payroll_recipients/{id}/adopt_pending_destination` — Save a recipient's submitted destination as their default — available

**Operation ID:** `payroll_recipients.adopt_pending_destination`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.write` · **Test mode:** unavailable

Confirm the destination a recipient saved through their own payout link (`pending_destination` on the
recipient) and make it their default: new pay runs copy it from then on. A destination a recipient submits
fills that one run's row and waits in `pending_destination` — it never replaces the employer's default on its
own. Moves no money.

Send `expected_submitted_at` = the `pending_destination.submitted_at` you showed. If the recipient saved a
different destination since, the answer is `409 pending_destination_changed` and the default is unchanged:
read the recipient again and show the new one. In a rare race — the recipient saves a new destination while
this call runs — rows this call already filled with the destination you confirmed keep it; the next read of
those runs shows them. `409 no_pending_destination` means nothing is waiting.

The same call fills this recipient's rows that still wait for a destination in pay runs not yet approved
(`draft`, `reviewed`), where the destination can pay the run's currency: those rows are listed in
`updated_run_items`. A row the destination cannot pay keeps asking the recipient and is listed in
`skipped_run_items` with a reason. A row that already has a destination, and every approved run, is left
alone; approval checks every row again. 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
admitted only with a key Swaps can confirm, as at every inbound door: `422 destination_wallet_disabled`, `422
destination_wallet_key_reported_lost` (its key was reported lost), `422 destination_wallet_signer_at_risk` or
`422 destination_wallet_signer_unconfirmed` (its key cannot be confirmed) otherwise. Those codes go only to the
wallet's own holder: a payer adopting a recipient's wallet hears `422 destination_not_accepted` («This address
can't take this transfer. Use another address.») for any of them — never the reason. When the wallet or
key-mark lookup
itself fails, every caller hears `503 destination_check_unavailable` (`temporarily_unavailable`; a holder whose
later read fails, `503 destination_risk_unverifiable`): nothing was written, retry after `Retry-After` with the
same `Idempotency-Key`.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [PayrollRecipientAdoptPendingDestinationRequest](/reference/~schemas.md#payrollrecipientadoptpendingdestinationrequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The recipient with its new default, and the run rows this call filled or left asking. | `application/json` [PayrollRecipientDestinationAdoption](/reference/~schemas.md#payrollrecipientdestinationadoption) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="list-templates-available"></a>
## `GET /payroll_templates` — List templates — available

**Operation ID:** `payroll_templates.list`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

List this employer's saved run templates.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `Limit` | component | — | — | Reusable parameter component |
| `Cursor` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | A page of templates. | `application/json` [PayrollTemplateList](/reference/~schemas.md#payrolltemplatelist) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="save-a-run-as-a-template-available"></a>
## `POST /payroll_templates` — Save a run as a template — available

**Operation ID:** `payroll_templates.create`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.write` · **Test mode:** unavailable

Save a completed run's current items as a reusable template. There is no operation to
start a new run from a template — nothing in the backend consumes `items_snapshot` back
into a run yet, so do not advertise that capability to a caller.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |
| `IdempotencyKey` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [PayrollTemplateCreateRequest](/reference/~schemas.md#payrolltemplatecreaterequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `201` | The created template. | `application/json` [PayrollTemplate](/reference/~schemas.md#payrolltemplate) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-a-template-available"></a>
## `GET /payroll_templates/{id}` — Get a template — available

**Operation ID:** `payroll_templates.get`

> **Status:** Available · **Callers:** business key, agent (business key), dashboard session · **Scope:** `payroll.read` · **Test mode:** unavailable

Return one saved template this caller owns.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `id` | path | yes | `string` | — |
| `SwapsVersion` | component | — | — | Reusable parameter component |
| `SwapsAccount` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The template. | `application/json` [PayrollTemplate](/reference/~schemas.md#payrolltemplate) |
| `400` | — | `BadRequest` |
| `401` | — | `Unauthorized` |
| `403` | — | `Forbidden` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="get-a-recipients-own-session-available"></a>
## `GET /payroll_recipient_sessions/{token}` — Get a recipient's own session — available

**Operation ID:** `payroll_recipient_sessions.get`

> **Status:** Available · **Callers:** public token · **Test mode:** unavailable

Resolve one run item's payer-facing projection from its capability token. No identity is
resolved and none is needed — the token is unguessable and scoped to exactly this item.
Pure: unlike a payment session, reading this never advances the item's status.

The token has a stated lifetime (14 days from issue) and is retired the moment it is used
to change a destination. An unknown token, an expired one and a retired one are
indistinguishable from here: all three answer `404`, so this endpoint cannot be used to
confirm that a leaked link was ever real. Ask the employer to re-issue the link.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `token` | path | yes | `string` | A capability token, never an object id — do not log it, store it beyond the session, or treat it as a stable identifier. |
| `SwapsVersion` | component | — | — | Reusable parameter component |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The recipient's session. | `application/json` [PayrollRecipientSession](/reference/~schemas.md#payrollrecipientsession) |
| `400` | — | `BadRequest` |
| `404` | — | `NotFound` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |

---

<a id="set-a-destination-available"></a>
## `POST /payroll_recipient_sessions/{token}/destination` — Set a destination — available

**Operation ID:** `payroll_recipient_sessions.destination.set`

> **Status:** Available · **Callers:** public token · **Test mode:** unavailable

Set where this item's payment lands — a bank rail or a crypto address. Only reachable
while the item is `needs_destination` or `ready`; once it moves past that, the session is
locked and this is refused with `409`. A CHANGE to an already-`ready` item is additionally
refused with `409` once the run itself has moved past `draft`/`reviewed` (`approved`
onward) — an approved/funded run's payout destination can no longer be swapped through
this link. A FIRST submission on a still-`needs_destination` item is unaffected by the
run's stage. The crypto branch answers `capability_unavailable` while that rail is closed
behind a flag, whatever fields are supplied. A bank destination on a run whose currency
the payout wallet does not hold (a EUR run funded into USDC) answers
`409 bank_payout_fx_unsupported` before anything is read or written — the session's
`capabilities.bank.reason` carries the same code, so a client never has to discover it
here. While the bank rail is off, or does not pay the run currency, a bank destination
answers `409 capability_unavailable` the same way (`capabilities.bank.reason`
`bridge_bank_rail_disabled` / `bank_currency_unsupported`). A crypto destination is a USDC
wallet unless `crypto.asset` is `EURC`; an EURC wallet is taken only where the session lists
it in `capabilities.crypto_external_wallet.assets` (otherwise `400 invalid_request`, as for
any asset the run is not paid in), and only on a network EURC is paid out on — EURC on
Polygon or Arbitrum answers `409 asset_network_unsupported` before anything is written.

A USD run may pay a local bank account instead, where the session lists its currency in
`capabilities.bank.local_currencies`: `bank.pix_key` (Brazil, BRL), `bank.clabe` (Mexico, MXN by
SPEI) or `bank.bre_b_key` (Colombia, COP), each with `bank.account_holder` only. The key is checked
before anything is sent (a CPF/CNPJ's or a CLABE's check digits); Bridge's rate is then read and
the item weighed against Bridge's minimum for the rail — `409 local_payout_below_minimum` (with
`details`) refuses it before the account is created. The item stays in USD; what the recipient
receives in the local currency is Bridge's to fix at payout — the session publishes the estimate
(`local_payout.estimate`) and, once paid, the delivered amount from Bridge's receipt. A local
currency the session does not list answers `409 capability_unavailable`.

A USD run's bank destination is a US bank account (ACH): `bank.account_number` (4 to 17
digits), `bank.routing_number` (9 digits with a valid ABA check digit), `bank.bank_account_type`
and the holder's US mailing address including `state`. The account is created with the
payout provider before anything is stored; only its last four digits are kept and
published back (`destination.last4`). A refusal by the provider answers
`422 beneficiary_invalid` — `param` names the one request field it refused, when it names
exactly one — and never repeats the provider's text. The same code with no `param` answers
details already on file with this payer that cannot be adopted: no single account matched,
or the one on file has the other account type or another holder name (it would be paid as
it is on file, so a correction is refused, not dropped). A provider outage or timeout answers
`503 temporarily_unavailable` with nothing stored. The same details sent again reach the
provider under the same idempotency key, so a retry never creates a second account.

This call moves money's destination, so it is bounded on three axes. The token must be
live: expired, retired and unknown tokens all answer `404`, never a distinguishable error.
The change is rate-limited per token — five in any rolling 24 hours, then `429`. And the
token is retired on success and replaced: read `rotated_token` off the response and use it
for every subsequent call.

`bank` must carry the identifiers of exactly one rail (`iban`, `clabe`, `pix_key`,
`sort_code`, or `account_number` + `routing_number`); an indeterminate set answers
`400 destination_rail_indeterminate` rather than being guessed at, and the rail is then
validated against the item's currency.

A crypto address that is a Swaps Wallet is taken only as every inbound door takes one — on
Tempo, with a key Swaps can confirm — before anything is written: `422 destination_rail_keyless`,
`destination_wallet_disabled`, `destination_wallet_key_reported_lost`,
`destination_wallet_signer_at_risk` or `destination_wallet_signer_unconfirmed`
(`invalid_request`; ask for another destination), and `503 destination_risk_unverifiable`
(`temporarily_unavailable`, `Retry-After`) when the check could not run — nothing was stored
and the token is not rotated. The recipient's link carries no sign-in, so it hears
`422 destination_not_accepted` for any of these codes, and `503 destination_check_unavailable`
when the wallet or key-mark lookup itself failed.

The destination set here applies to THIS run item. It does not change the employer's
stored default for the recipient — that stays an employer decision, and the employer is
notified that a change is waiting.

No `Idempotency-Key` is accepted: this route is dispatched off the public-token table,
which never reaches the account-scoped idempotency reservation an authenticated route
gets. The token itself is the real idempotency boundary — it is retired and rotated to
`rotated_token` on every successful submission. A concurrent duplicate submission with the
same token loses the compare-and-swap on that token and answers `409 recipient_locked`
(indistinguishable from a genuine state-machine conflict); exactly one write lands. If the
success response itself is lost in transit, the caller never receives `rotated_token`, the
old token is already dead, and neither a further update nor a `GET` with it succeeds —
there is no replay store, so the recipient can only get a fresh link from the employer.

### Parameters

| Name | In | Required | Schema | Description |
| --- | --- | --- | --- | --- |
| `token` | path | yes | `string` | A capability token, never an object id. |
| `SwapsVersion` | component | — | — | Reusable parameter component |

### Request body

Required: **yes**

| Media type | Schema |
| --- | --- |
| `application/json` | [PayrollRecipientDestinationRequest](/reference/~schemas.md#payrollrecipientdestinationrequest) |

### Responses

| Status | Description | Body |
| --- | --- | --- |
| `200` | The updated session. | `application/json` [PayrollRecipientSession](/reference/~schemas.md#payrollrecipientsession) |
| `400` | — | `BadRequest` |
| `404` | — | `NotFound` |
| `409` | — | `Conflict` |
| `413` | — | `PayloadTooLarge` |
| `422` | — | `UnprocessableEntity` |
| `429` | — | `TooManyRequests` |
| `500` | — | `InternalError` |
| `503` | — | `ServiceUnavailable` |
