> ## Documentation Index
> Fetch the complete documentation index at: https://docs.infinite.net/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> If you are an AI assistant operating Infinite for a user, read the skill first: https://docs.infinite.dev/agent-skill.md (setup per host, tool reference, rules for moving money, journeys).
> When operating Infinite for a user, use the MCP server. Sign in at https://bot.api.infinite.dev/mcp (production, acts as the user) or send `Authorization: Bearer <key>` to https://bot.api.infinite.dev/mcp/sse. Prefer its tools over raw HTTP; use REST only for steps no tool covers, and check `get_endpoint_schema` before guessing a request shape.
> Net-new Infinite Connect (org_connect) is operator-assisted: do not grant end users shared org_connect Auth Kit membership or the shared platform org API key — those credentials are org-wide on /v1. BYO org keys onboard under that customer's org id. Operator sessions should show org_connect in get_organization_info.
> The key decides the tools. Every organization key (`sk_sandbox_…` / `sk_live_…`) gets docs, onboarding, `get_account_balance`, and request logs for your own organization. BYO organizations add payees an agent will pay with `create_payee` (payable once ACTIVE). Platform Connect sessions instead get bound-customer payment lookups (`get_quote`, `get_transfer`, `list_transfers`, `list_account_balances`), payee drafts (`create_payee_draft` / `get_payee_draft`; screening only, never payable), transfer drafts that wait for Infinite approval (`create_transfer_draft` / `get_transfer_draft`; never POST /v1/transfers), and with a bound customer, `create_transfer` (real POST /v1/transfers, forced IN_REVIEW until Infinite ops approve; not on BYO org keys). An agent key (`sk_sandbox_agent_…` / `sk_live_agent_…`) on a **PAYMENT**-role agent moves money within that agent's limits: `create_transfer` (draft) → `submit_transfer` → `wait_for_transfer`, without per-payment Infinite approval. **PAYMENT_OPERATIONS** agent keys are read-only (no `get_quote`, `create_transfer`, or `submit_transfer`; use `get_quote_by_id` and `list_transfers` on the pinned customer).
> On sandbox organization-key sessions, `sandbox_approve_customer` (makes the capabilities you pass READY, and related ones may turn READY too; `request_capabilities` is not needed), `sandbox_fund_account`, and `sandbox_complete_transfer` stand in for verification, funding, and settlement, so a whole flow runs from MCP. BYO keys can pass `customerId` on each call without binding; platform sessions must bind a customer first (`create_business_customer` auto-binds, or send `X-Infinite-On-Behalf-Of`).
> When searching documentation without a key, use the docs MCP at https://docs.infinite.dev/mcp.
> Start in sandbox (https://sandbox.api.infinite.dev). Do not use production unless the user explicitly asks.
> Confirm amount, currency, and payee with the user before any payment is submitted. Pass the invoice number as `reference` on `create_transfer`; the summary says what the payee will see.
> REST amounts are integer minor units (`"4000"` = 40.00 USD). MCP payment tools take exact decimal strings (`"40.00"`). Never mix them.
> Agent denials return `403 { denial: { code, message, window, limit, used, remaining, attemptedAmount, currency }, hint }` with decimal amounts; per-transfer denials omit `used` and `remaining`. Report them to the user; do not work around them.

# Invoice to pay

> Accounts payable playbook — intake a supplier invoice, allowlist the payee, pay under an agent identity, then reconcile

Pay a supplier from a funded customer account using the same objects as any other payout — [customers](https://docs.infinite.net/concepts/customers.md), [counterparties](https://docs.infinite.net/api-reference/counterparties/create-a-counterparty.md), [transfers](https://docs.infinite.net/concepts/transfers.md), and [webhooks](https://docs.infinite.net/guides/webhooks.md) — with an [**agent**](https://docs.infinite.net/concepts/agents.md) (`agt_`) bound to that customer, source account, spend limits, and a hard destination allowlist enforced on the agent key.

This is the accounts payable path for an agent key. There is no separate “pay invoice” API. Compose the steps below.

> [!NOTE]
>
> [Agents](https://docs.infinite.net/concepts/agents.md) (customer pin, source-account pin, destination allowlist, spend limits) are **early access** — ask your account team to enable them (`403 agents_not_enabled` until then). Instruction-audit export is **beta**.

## Flow

```mermaid theme={null}
flowchart LR
  A[Invoice / vendor] --> B[Counterparty + external account]
  B --> C{external_account.approved}
  C -->|yes| D[Add eba_ / ewa_ to agent allowlist]
  D --> E["POST /v1/transfers under agent key"]
  E --> F[transfer.* webhooks + instruction-audit]
  C -->|no| G[Do not pay]
```

| Step | What you do | Why |
| - | - | - |
| 1. Intake | Capture vendor + bank details (and optional invoice document) | Evidence for screening and audit |
| 2. Register payee | Create counterparty + external account | Destinations start `IN_REVIEW` |
| 3. Wait for approval | Subscribe to `external_account.approved` | Unknown / rejected payees must not be paid |
| 4. Open perimeter | Add the approved account id to the agent allowlist | Hard allowlist denies anything else |
| 5. Pay | `POST /v1/transfers` with the agent key | Network enforces pins + allowlist |
| 6. Reconcile | Webhooks, `GET /v1/transfers/{id}`, instruction-audit | Case file for every accepted payout |

## Prerequisites

* Verified business [customer](https://docs.infinite.net/concepts/customers.md) in `ACTIVE`
* Dedicated funded [account](https://docs.infinite.net/concepts/accounts.md) for the agent (balance account, treasury wallet, or other payable source your corridor supports) — see [Balance accounts](https://docs.infinite.net/guides/balance-accounts.md)
* An [agent](https://docs.infinite.net/concepts/agents.md) in the default **`PAYMENT`** role (not **`PAYMENT_OPERATIONS`**, which is read-only and cannot create or submit transfers) created for that customer + source account with a **non-null** destination allowlist (`[]` until suppliers are approved), and an **agent API key** (`sk_*_agent_*`) issued from the console **Agents** page or `POST /v1/agents/{id}/keys` (see [Credentials and scopes](#credentials-and-scopes)) — do not reuse broad org keys for payouts
* Webhook endpoint subscribed to payee, agent, and transfer events ([Webhooks](https://docs.infinite.net/guides/webhooks.md))

Until agents are enabled for your org, you cannot use this playbook’s network perimeter — keep automation behind your own controls.

> [!WARNING]
>
> `allowedDestinationAccountIds: null` on the agent (the default when unset) still allows **every** valid destination owned by the pinned customer. Before issuing the agent key, set a **non-null** allowlist on the agent — start with `[]` (deny all) and add approved `eba_…` / `ewa_…` ids, **or** withhold the key secret until the allowlist is saved. Empty `[]` denies all destinations; only listed ids are payable.

## Credentials and scopes

`transfers:write` alone is **not** enough for this playbook. Split **operator / JWT** work (open the perimeter, register payees, attach documents) from the **restricted agent key** (submit allowlisted payouts).

| Phase | Credential | Permissions (minimum) |
| - | - | - |
| 1. Document upload / confirm | Operator API key **or** JWT | `customers:write` (`transfers:write` only if you pass `transferId` on create) |
| 2. Find / reuse payee (list counterparties + accounts) | Operator API key **or** JWT | `accounts:read` — write does **not** imply read |
| 2. Create counterparty + external account | Operator API key **or** JWT | `accounts:write` |
| 3. Poll payee / account status | Operator API key **or** JWT (preferred over agent key) | `accounts:read` |
| 4. Read current allowlist | **Agent key** (own `agt_` only) **or** unconstrained org key / JWT | `apiKeys:read` (`GET /v1/agents/{id}`) |
| 4. Update allowlist | **Unconstrained** organization API key **or** JWT — **not** the agent key | `apiKeys:write` (`PATCH /v1/agents/{id}` with `allowedDestinationAccountIds`; API-only today — see step 4) |
| 5. Create / submit transfer | **Agent API key** on a **`PAYMENT`**-role agent | Fixed scopes include `transfers:write` on payment agents (`PAYMENT_OPERATIONS` agents are read-only) |
| 6. Poll transfer / recovery list | Operator API key **or** JWT (preferred) | `transfers:read` — org-wide list on organization keys (see below) |
| 6. Instruction-audit export | Operator API key **or** JWT | `transfers:read` (base). Full package sections may need `apiKeys:read` (legacy key-policy dump) and optionally `accounts:read` / `cases:read` / `members:read` |

> [!WARNING]
>
> Agent **pins and allowlists only gate transfer operations** (`create` / `confirm` / `submit` / related money-movement). They do **not** filter reads on organization API keys.
>
> **Agent keys** use **fixed scopes by MCP role** on the agent:
>
> | Role | Scopes | Payout writes |
> | - | - | - |
> | **`PAYMENT`** (default) | `transfers:read`, `transfers:write`, `accounts:read`, `customers:read`, `apiKeys:read` | Allowed (subject to pins, allowlist, limits) |
> | **`PAYMENT_OPERATIONS`** | `transfers:read`, `accounts:read`, `customers:read`, `apiKeys:read` — **no** `transfers:write` | Denied (`agent_read_only`) |
>
> **Read scoping on agent keys:** account list/get and customer get are limited to the agent’s **principal customer**. Transfer list/get is limited to that agent’s transfers (**`PAYMENT`**) or the pinned customer’s transfers (**`PAYMENT_OPERATIONS`**). **`GET /v1/counterparties` without `customerId` is organization-wide** even on agent keys — for payee reuse in this playbook, always list with **`customerId=`** (operator or agent). Agent keys **cannot** call `PATCH /v1/agents/{id}` or `/v1/internal/api-keys/.../policy`.
>
> **Organization API keys and JWT sessions** use normal scope grants. `accounts:read` and `transfers:read` on those credentials list **organization-wide** data — every customer in the org, not only the pinned customer. In multi-customer orgs, keep `clientReferenceId` recovery and cross-customer reconciliation on an **operator** credential with intentional scope, not on assumptions that agent pins hide other customers’ rows.

**Least privilege**

* **Agent key (`PAYMENT` role):** inherits customer + source pins and allowlist from the agent; scopes are fixed (above). Do **not** hand out the key secret while the agent’s `allowedDestinationAccountIds` is still `null`.
* **Operator / organization key or JWT:** human or trusted automation that registers suppliers, uploads invoice documents, updates the agent allowlist via **`GET` / `PATCH /v1/agents/{id}`** (`apiKeys:read` / `apiKeys:write`), and polls / recovers transfers with org-wide reads when needed. Payee reuse needs **`accounts:read` and `accounts:write`**.
* Webhooks do not consume API scopes; they require a verified endpoint secret.

## 1. Invoice / vendor intake

Two valid starts:

**Known vendor** — you already have legal name, bank (or wallet) details, and amount. You may skip document upload and go to [Register the payee](#2-register-the-payee) **only** when your payout corridor does **not** require supporting documents. Third-party **SWIFT** still needs at least one `documentIds` entry on transfer create (see step 5) — upload/confirm the invoice first even for a known vendor.

**Invoice evidence** — upload supporting documents via the public Documents API, then attach them on the transfer with `documentIds`. Create the upload request with **all** of these fields (there is no `category` field):

| Field | Example |
| - | - |
| `documentType` | `INVOICE` |
| `customerId` | `cus_abc123` |
| `fileName` | `invoice-2026-001.pdf` |
| `contentType` | `application/pdf` |

1. `POST /v1/documents` — body as above; see [Create document upload request](https://docs.infinite.net/api-reference/documents/create-document-upload-request.md)
2. Upload bytes to the returned URL
3. `POST /v1/documents/{id}/confirm` — [Confirm document upload](https://docs.infinite.net/api-reference/documents/confirm-document-upload.md)
4. Keep the `doc_` id for the transfer

Structured invoice parse (vendor / amount / bank prefill) is available in the console transfer-create flow and as an **internal** assist API. It does **not** create counterparties or move money. Agents and integrators should treat parse output as suggestions and still register payees through the counterparty + external-account path below.

> [!WARNING]
>
> Console **invoice.**\* webhooks are accounts-receivable (you are collecting). They are not this supplier-payout playbook. Use `transfer.*` and `external_account.*` for accounts payable.

## 2. Register the payee

Do **not** always create a new counterparty. For repeat invoices to the same supplier, find and reuse the existing counterparty and external bank account or wallet first — list counterparties with **`customerId=`** (paying customer) and list accounts for that customer; match on bank or wallet identity. Do not rely on an unfiltered counterparty list on an agent key — it is organization-wide.

**This playbook covers bank accounts (`eba_…`) and external wallets (`ewa_…`) only.** Do **not** use proxy destinations (`epa_…` / alias rails such as PIX, UPI, PromptPay) here — proxy create does not preserve the supplier `counterpartyId` binding this flow requires, and proxy rail reuse rules differ. Prefer a bank or wallet payee for AP payouts.

External-bank / external-wallet create **deduplicates** by bank or wallet identity for that customer — **not** by requested rail, chain, or currency. If the same identifiers already exist:

* The API returns the existing account with `created: false` (no new screening cycle)
* Create does **not** merge capability fields from your request onto the reused row
* For **bank accounts and wallets**, passing a **different** `counterpartyId` than the one already linked is **rejected**
* Already-`ACTIVE` / approved reused accounts emit **no** new `external_account.approved` — do not wait forever for a webhook that will not fire; branch on the create response

On every create/reuse response, check `created` and `status`, then the fields that matter for **this** payout:

| Destination | Check on reuse | If the payout capability is missing |
| - | - | - |
| Bank account (`eba_…`) | `paymentRails` | `PATCH /v1/external-bank-accounts/{id}` with the **full** compatible `paymentRails` list (existing plus any to add). A WIRE-only reused account fails ACH until ACH is added |
| External wallet (`ewa_…`) | `chains` and `currencies` | `PATCH /v1/external-wallets/{id}` with the **full** desired `chains` / `currencies` lists. Create reuse does not merge them — a BASE-only reused wallet fails an Ethereum payout until Ethereum is added |

Do not rely on a second create to widen rails, chains, or currencies.

```mermaid theme={null}
flowchart TD
  Lookup[Find existing cp_ + eba_/ewa_ for supplier] -->|found approved| Reuse[Reuse ids — skip to allowlist if needed]
  Lookup -->|not found| CreateCp[Create counterparty]
  CreateCp --> CreateAcct[Create external account / wallet]
  CreateAcct -->|created true| Wait[Wait for external_account.approved]
  CreateAcct -->|created false + approved status| Reuse
  CreateAcct -->|created false + still IN_REVIEW| Wait
```

When you do create, supplier destinations are **third-party** bank accounts or wallets. Create defaults `isThirdParty` to `false` and **rejects** `counterpartyId` unless `isThirdParty` is true — omitting these fields misclassifies the supplier destination as the customer’s own account.

Set all of:

| Field | Value |
| - | - |
| `customerId` | The paying customer (`cus_…`) — the principal whose funds move |
| `isThirdParty` | `true` |
| `counterpartyId` | The existing or newly created `cp_…` for that supplier |

New rows (`created: true`) start **`IN_REVIEW`** and are not payable until screening finishes. On reuse (`created: false`), inspect `status` **and** the capability fields above (`paymentRails`, or `chains` / `currencies`) before waiting on webhooks or submitting a transfer.

For crypto → fiat SWIFT supplier payouts, see [SWIFT payouts](https://docs.infinite.net/guides/swift-payouts.md) for bank-account field shape.

## 3. Wait for destination approval

Subscribe to:

| Event | Meaning |
| - | - |
| `external_account.in_review` | Destination entered screening |
| `external_account.approved` | Safe to allowlist and pay |
| `external_account.rejected` | Do **not** add to allowlist; do not pay |
| `counterparty.created` / `counterparty.updated` | Counterparty metadata only — not payability |

Poll `GET /v1/accounts/{id}` (or list accounts for the customer) if you need authoritative status. On account-create reuse (`created: false`), use the returned `status` — do not block on a new approval webhook for an already-approved destination. Do not race the agent key against an unapproved destination: even if you omit allowlist updates, other validation may still reject the transfer.

## 4. Add the destination to the allowlist

Only after `external_account.approved`, add that account id (`eba_…` / `ewa_…`) to the **agent** (`agt_…`). Use an **organization API key** or JWT org session — **not** the agent key (`sk_*_agent_*` cannot update agents or legacy key policy). Do not allowlist proxy (`epa_…`) destinations for this playbook.

* **Console (read-only today):** **Agents** → select the agent → **Payees** shows the current allowlist but has **no editor** — use the API below until console editing ships.
* **API:** `GET /v1/agents/{id}` with the **agent key** (read own allowlist) or operator credential (`apiKeys:read`), then `PATCH /v1/agents/{id}` with an **unconstrained** org key or JWT (`apiKeys:write`) and the full `allowedDestinationAccountIds` array

> [!WARNING]
>
> `allowedDestinationAccountIds` on `PATCH /v1/agents/{id}` **replaces** the entire allowlist — it does not append. Read the agent first, then send the **complete** desired array including the new id. Sending only the new id drops every previously allowlisted destination.

> [!WARNING]
>
> Allowlist read-modify-PATCH has **no version / CAS**. Concurrent updates can stale-write and resurrect a supplier another operator just removed. Treat allowlist edits as a **security perimeter**: use a **single serialized writer** (one ops queue, lock, or equivalent concurrency control) per agent. Do not fan out parallel GET→PATCH jobs against the same agent.

Keep the allowlist small. Agents accept at most **50** destination IDs — larger supplier sets need pruning, multiple segmented agents/keys, or another perimeter design. An empty allowlist **denies all** destinations. **`PAUSED`** blocks new agent **submit** paths — `POST /v1/transfers` with `confirm: true` and `POST /v1/transfers/{id}/submit` (including submitting an existing DRAFT) — but does **not** stop in-flight processing of transfers already submitted, and does **not** block other transfer writes such as **`PATCH /v1/transfers/{id}/fund-from-balance`** on an existing pending crypto transfer (agent policy pins still apply). After **credential compromise**, **revoke** the agent key or agent — pausing alone leaves the key authenticated with scoped reads.

> [!NOTE]
>
> Changing a supplier’s bank details means a **new** external account (new id), a new approval, and an allowlist update. Treat bank swaps as a controlled perimeter change, not an in-place edit the agent can invent.

## 5. Create the transfer under the pinned key

Call `POST /v1/transfers` with the **agent** API key (not an organization key).

Required discipline:

| Field / header | Rule |
| - | - |
| `Authorization` | Agent key (`sk_*_agent_*`) for the `agt_` with allowlist set |
| `Idempotency-Key` | Unique high-entropy value per logical payout — [Idempotency](https://docs.infinite.net/guides/idempotency.md) |
| `clientReferenceId` | **Organization-wide unique** id for this logical payout (namespace by agent + customer + invoice/voucher, e.g. `ap:{agent}:{cus_…}:{voucher}`). Required for safe recovery after timeouts or cached 5xx — list filters the **whole org** by exact match and the API does **not** enforce uniqueness. Caller-local uniqueness is insufficient when two agents can collide |
| `onBehalfOf` | Pinned customer id |
| `source.accountId` | Pinned source account |
| `destination.accountId` | Allowlisted, approved external account |
| `counterpartyId` | Matching counterparty |
| `documentIds` | **Required** when `destination.paymentRail` is `SWIFT` for third-party / supplier payouts (API rejects without at least one supporting document). Optional but recommended for other rails when paying from invoice evidence |
| `confirm` | `true` to submit immediately, or omit / set false to leave a `DRAFT` you (or the same key) can later `POST /v1/transfers/{id}/submit` |

> [!NOTE]
>
> For this playbook’s supplier SWIFT path, upload and confirm the invoice document first, then pass its `doc_…` in `documentIds`. See [SWIFT payouts](https://docs.infinite.net/guides/swift-payouts.md) for rail-specific destination fields.

Example (USD balance → ACH supplier):

```python Python theme={null}
import requests

response = requests.post(
    'https://api.infinite.dev/v1/transfers',
    headers={
        'Authorization': 'Bearer sk_live_agent_...',
        'Content-Type': 'application/json',
        'Idempotency-Key': '9f3c2a1b-4d5e-6f70-8192-a3b4c5d6e7f8',
    },
    json={
        'onBehalfOf': 'cus_abc123',
        'clientReferenceId': 'ap:ops:cus_abc123:inv-2026-001',
        'amount': {
            'currency': 'USD',
            'rawValue': '12500',
        },
        'source': {
            'currency': 'USD',
            'paymentRail': 'BALANCE',
            'accountId': 'vba_agent_balance',
        },
        'destination': {
            'currency': 'USD',
            'paymentRail': 'ACH',
            'accountId': 'eba_vendor456',
        },
        'counterpartyId': 'cp_vendor456',
        'documentIds': ['doc_invoice789'],
        'paymentReason': 'PAYMENT_FOR_GOODS_AND_SERVICES',
        'confirm': True,
    },
)
```

```typescript TypeScript theme={null}
const response = await fetch('https://api.infinite.dev/v1/transfers', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_live_agent_...',
    'Content-Type': 'application/json',
    'Idempotency-Key': '9f3c2a1b-4d5e-6f70-8192-a3b4c5d6e7f8',
  },
  body: JSON.stringify({
    onBehalfOf: 'cus_abc123',
    clientReferenceId: 'ap:ops:cus_abc123:inv-2026-001',
    amount: {
      currency: 'USD',
      rawValue: '12500',
    },
    source: {
      currency: 'USD',
      paymentRail: 'BALANCE',
      accountId: 'vba_agent_balance',
    },
    destination: {
      currency: 'USD',
      paymentRail: 'ACH',
      accountId: 'eba_vendor456',
    },
    counterpartyId: 'cp_vendor456',
    documentIds: ['doc_invoice789'],
    paymentReason: 'PAYMENT_FOR_GOODS_AND_SERVICES',
    confirm: true,
  }),
});
```

```bash CLI theme={null}
export INFINITE_API_KEY=sk_live_agent_...
infinite api POST /v1/transfers \
  --header 'Idempotency-Key: 9f3c2a1b-4d5e-6f70-8192-a3b4c5d6e7f8' \
  --body '{
  "onBehalfOf": "cus_abc123",
  "clientReferenceId": "ap:ops:cus_abc123:inv-2026-001",
  "amount": {
    "currency": "USD",
    "rawValue": "12500"
  },
  "source": {
    "currency": "USD",
    "paymentRail": "BALANCE",
    "accountId": "vba_agent_balance"
  },
  "destination": {
    "currency": "USD",
    "paymentRail": "ACH",
    "accountId": "eba_vendor456"
  },
  "counterpartyId": "cp_vendor456",
  "documentIds": ["doc_invoice789"],
  "paymentReason": "PAYMENT_FOR_GOODS_AND_SERVICES",
  "confirm": true
}'
```

```text MCP theme={null}
# Agent MCP create_transfer does not send clientReferenceId or documentIds — use Python, TypeScript, CLI, or cURL for this playbook (SWIFT invoice evidence + org-unique clientReferenceId recovery). See /guides/mcp for simpler agent-key payouts.
```

```bash cURL theme={null}
curl -X POST "https://api.infinite.dev/v1/transfers" \
  -H "Authorization: Bearer sk_live_agent_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9f3c2a1b-4d5e-6f70-8192-a3b4c5d6e7f8" \
  -d '{
    "onBehalfOf": "cus_abc123",
    "clientReferenceId": "ap:ops:cus_abc123:inv-2026-001",
    "amount": {
      "currency": "USD",
      "rawValue": "12500"
    },
    "source": {
      "currency": "USD",
      "paymentRail": "BALANCE",
      "accountId": "vba_agent_balance"
    },
    "destination": {
      "currency": "USD",
      "paymentRail": "ACH",
      "accountId": "eba_vendor456"
    },
    "counterpartyId": "cp_vendor456",
    "documentIds": ["doc_invoice789"],
    "paymentReason": "PAYMENT_FOR_GOODS_AND_SERVICES",
    "confirm": true
  }'
```

`rawValue` is minor units (cents for USD). For quotes, FX, and multi-rail patterns see [Quotes and transfers](https://docs.infinite.net/guides/quotes-and-transfers.md). For crypto deposit → SWIFT fiat, see [SWIFT payouts](https://docs.infinite.net/guides/swift-payouts.md).

### Policy deny paths (expected)

When the agent has pins and allowlist set, the network **fail-closes**:

| Attempt | Typical result |
| - | - |
| Destination not on allowlist | Denied — destination not authorized |
| Wrong `source.accountId` | Denied — source pin |
| Wrong `onBehalfOf` / customer | Denied — customer pin |
| Empty allowlist | Denied — all destinations |
| Revoked agent / key | Auth failure |

Denied creates do **not** produce a transfer id and are **not** in instruction-audit export. Capture your request id and error body for ops review.

> [!WARNING]
>
> `POST /v1/transfers` [idempotency](https://docs.infinite.net/guides/idempotency.md) caches **failed responses for about 24 hours** — not only 4xx. Client errors (allowlist/pin denials), **5xx**, and other recorded failures are returned again on the same `Idempotency-Key`. Reusing the key after a definitive cached failure only returns that cached error; it does not re-run create. After that window, the same key no longer short-circuits create.
>
> Always send an **organization-wide unique** `clientReferenceId` on the original create — namespace so two agents (or two customers) cannot collide (e.g. `ap:{agentId}:{customerId}:{voucher}`). Public transfer list supports exact `clientReferenceId` filter across the **whole organization**; optional `customerId` narrows the list, but you must still verify the returned transfer’s amount / destination / `sourceCustomerId` match the payout you intended (`sourceCustomerId` is the paying customer on list/get responses — the create body field is `onBehalfOf`). The API does **not** filter by amount and does **not** enforce uniqueness. Do **not** decide “no transfer exists” from customer/amount/time heuristics; that can miss a committed payout and a new idempotency key will **duplicate** the supplier payment.
>
> **Serialize recovery per logical payout.** An empty `clientReferenceId` list is not a distributed lock. Two operators recovering the same voucher concurrently can both see empty results and `POST` with **different** `Idempotency-Key` values — idempotency only dedupes identical keys, and `clientReferenceId` is not unique — so you get a **double pay**. Coordinate recovery (single writer, lease/lock, or queue) so only one writer runs list → decide → create for that payout at a time.
>
> On any ambiguous or delayed outcome (timeout, unknown whether create finished, or any non-immediate retry): **list by exact `clientReferenceId` first** before reusing or replacing the `Idempotency-Key`. A transfer can already exist even when replaying the key does not return a cached success. Prefer reusing the **original** key after an empty list while still within the \~24h failure-cache window; mint a new key only after another empty list when you must. Past that window, POST only after an empty list.
>
> | Outcome | What to do |
> | - | - |
> | Confirmed **4xx** denial | Remediate policy (or request). `GET /v1/transfers?clientReferenceId=…` (operator credential; optionally `&customerId=…`) should be empty; then retry with a **new** `Idempotency-Key` (same org-unique `clientReferenceId` is fine) |
> | Confirmed **cached 5xx** / recorded server failure | Do **not** keep replaying the same key. Under a **single serialized recovery writer** for this payout: exact-list `GET /v1/transfers?clientReferenceId=<id from original request>` (prefer also `customerId=`). If any transfer is returned, verify amount / destination / `sourceCustomerId`, then **stop** — reconcile that payout; do not create again. Only if the list is empty may you retry with a **new** `Idempotency-Key` |
> | Ambiguous timeout / unknown whether create finished | **Do not** reuse the `Idempotency-Key` first. Under a **single serialized recovery writer**: exact-list `GET /v1/transfers?clientReferenceId=…` (operator; optionally `customerId`). If a transfer exists, verify amount / destination / `sourceCustomerId`, then **stop** and reconcile. Only if empty: within the \~24h failure-cache window prefer reusing the **original** key once; if that returns a definitive cached failure, mint a **new** key only after another empty list. Past \~24h, POST only after an empty list. Never run this path concurrently with another recoverer for the same voucher |

Agent keys also cannot open other blast-radius surfaces (for example global payout batches, agent management, or fixed-route create). Use direct `POST /v1/transfers` for this playbook.

## 6. Reconcile and audit

| Signal | Use |
| - | - |
| `transfer.created` / `transfer.processing` / `transfer.updated` / `transfer.completed` / `transfer.failed` / `transfer.cancelled` / `transfer.refund_initiated` / `transfer.refund_completed` / `transfer.reversed` | Push lifecycle — [Webhooks](https://docs.infinite.net/guides/webhooks.md). Settled bank returns that reverse a payout typically emit **`transfer.reversed`**. Some other refund flows emit **cancelled** and/or **refund\_**\* while status becomes `REVERSED`. **Not every** path to `REVERSED` emits a customer webhook (some operational status corrections are silent) — subscribe to **`transfer.reversed`** where applicable **and** keep periodic `GET` reconciliation. |
| `GET /v1/transfers/{id}` | Authoritative status (webhooks can lag or omit transitions, including silent operational corrections). **Required** after completion — do not infer terminal state from webhooks alone. A single post-completion GET (or polling only until the first terminal status) is **not** enough: status can later become `REVERSED` with no event. Schedule **periodic re-fetches** through your accounting reconciliation window (for example daily or at period close) for recently completed payouts until books are closed. |
| `GET /v1/internal/transfers/{id}/instruction-audit` | One beta case file: request context, policy checks (when attributable), screening/holds, route, settlement, events. Requires `transfers:read`; include `apiKeys:read` to see `apiKeyPolicy` (see [Credentials and scopes](#credentials-and-scopes)). |

When a transfer is in `IN_REVIEW`, poll until a terminal **status** on `GET /v1/transfers/{id}`: `COMPLETED`, `REVERSED`, `FAILED`, or `CANCELLED`. Some monitoring alerts do **not** hold funds — do not treat every compliance signal as a hard stop without checking transfer status. After first completion, keep re-checking status on your reconciliation schedule so a later silent `REVERSED` is not missed.

## Operator checklist

1. Fund only the pinned source account
2. Human-vet suppliers; keep allowlist ≤ the set finance approved (**max 50** destination IDs per key)
3. Agent: customer pin + source pin + **non-null** destination allowlist (`[]` until approved) — never allowlist-`null` on a multi-customer org
4. Prove deny paths before production volume (unknown dest, empty allowlist, wrong source, wrong customer, revoked key)
5. Export instruction-audit for sample completed payouts; schedule periodic `GET /v1/transfers/{id}` through the accounting reconciliation window (silent `REVERSED` has no webhook)
6. **Revoke** compromised agent keys (`DELETE /v1/agents/{id}/keys/{keyId}`) or the agent — do not rely on **`PAUSED`** alone after compromise (paused keys remain authenticated). Use **`PAUSED`** when you need to block new **`confirm: true`** creates and **submit** on drafts while existing submitted transfers keep processing; it is not a substitute for revocation after compromise

## Out of scope for this playbook

| Topic | Status |
| - | - |
| Proxy payees (`epa_…` / alias rails) | **Out of scope** — proxy create does not preserve supplier `counterpartyId` for this flow; use bank (`eba_…`) or wallet (`ewa_…`) payees |
| Self-serve supplier-change productization | Later Phase 1 suite work — today: new external account → approve → allowlist |
| Dual-control / amount thresholds | Optional later. A `DRAFT` transfer is only a **procedural** pause — a **`PAYMENT`**-role agent key with `transfers:write` can still `confirm: true` or submit its own draft; that is not a human-only auth boundary |
| Cards / scoped card credentials | Deferred |
| Marketplace seller settlement / dispute engine | Separate SKU |
| Public “pay this invoice” resource | Not offered — compose Documents + transfers |

## Next steps

> [card] [Agents](https://docs.infinite.net/concepts/agents.md)
>
> Map wallet, pay, identity, and observability to live APIs

> [card] [Webhooks](https://docs.infinite.net/guides/webhooks.md)
>
> Signed events for payees and transfers

> [card] [Quotes and transfers](https://docs.infinite.net/guides/quotes-and-transfers.md)
>
> Amounts, quotes, and execution patterns

> [card] [SWIFT payouts](https://docs.infinite.net/guides/swift-payouts.md)
>
> Crypto to fiat supplier wires

> [card] [Idempotency](https://docs.infinite.net/guides/idempotency.md)
>
> Safe retries for agent writers

> [card] [Error handling](https://docs.infinite.net/guides/error-handling.md)
>
> Read API errors without guessing


## Related topics

- [Agents](https://docs.infinite.net/concepts/agents.md)
- [Infinite skill for AI assistants](https://docs.infinite.net/agent-skill.md)
- [MCP Server](https://docs.infinite.net/guides/mcp.md)
- [Pay back a credit line position](https://docs.infinite.net/api-reference/liquidity/pay-back-a-credit-line-position.md)
- [Payments](https://docs.infinite.net/coverage/payments.md)
