The flow of funds is chosen when you create the company (user funded or company balance). A company owner can switch models from the dashboard’s Company balance page, but only until the company first moves money (a card is funded, a transfer runs, or a deposit lands); after that the choice locks. Companies on the company balance model get the full test-mode loop immediately and free; live usage (the real pool, real transfers) requires an active paid subscription, the same one that unlocks production clients.
The flow of funds

Company balance: from create_card to the agent paying the merchant
- A connected user’s agent calls
create_cardwith the company flow. Agentcard checks your pool covers the amount (plus fees) and reserves it. - You receive a
card_flow.startedwebhook. This is your moment to collect: place a hold on your customer’s payment method in your own billing system. - You report the outcome with the
confirm_collectiontool — outcomesucceeded, orfailedto release the reservation. - Agentcard transfers the amount from your balance into the user’s spending power (about 30 seconds on-chain) and emits
transfer.initiated, thentransfer.completed. - Capture your customer’s payment when you receive
transfer.completed. From this point the funding is final: even if the card mint failed afterward, the money stays as that user’s spending power and automatically offsets their next card (you would seecard_flow.failedwithfunds_status: released_to_headroom). - The card mints and
card.createdfires with thetransfer_id, then the agent pays the merchant as usual. - If the card closes with unused balance,
transfer.releasedfires so you can release or refund your own hold. The unused amount stays as that user’s spending power and reduces what your pool sends on their next card. For a user you won’t fund again, pull it back into your pool with a recovery.
ack_mode: auto_approve (dashboard or CLI): step 2 still fires as a webhook, but the transfer proceeds without waiting.
Fund the balance
Set up the balance once (dashboard: Company balance → Set up balance, oragent-cards companies balance provision --org <org-id>). Then fund it two ways — both credit the same pool and fire the same wallet.funded webhook:
- Apple Pay / Google Pay (dashboard → Company balance → Add funds, admins only). A hosted card checkout — no crypto wallet needed. Any debit or credit card inside Apple/Google Pay works (a Mercury card, for example). The payment provider may run a quick identity check on the payer the first time. 10,000 per transaction; no lifetime purchase caps. Agentcard covers the provider fee (up to a cap), so the full amount lands.
- USDC on Base — send to the pool’s deposit address. No caps; this is the rail for recurring, treasury-scale funding.
wallet.balance.low before a card creation ever fails for insufficient funds.
Creating the live pool and funding it both require the active subscription; without one these calls return 403 subscription_required (test credentials and the mock pool keep working). Subscribe from the dashboard’s Go live page.
The create_card protocol
The end user’s agent calls the samecreate_card it always has. Two additions:
- An optional
funds_source(onramp_floworcompany_flow). Most companies set their default funds source tocompany_flowso agents never pass it. - While funding is in flight,
create_cardreturns202 { "status": "funding_in_progress", "transferId": "owt_…", "retry_after_seconds": 10 }. The agent retries with the same arguments and attaches to the in-flight funding, returning the card when it is ready. A retry that lands after the mint still returns that card (for about 2 minutes, while it is unused) instead of opening a second funding. Declined or timed-out collections surface as422 funding_not_approved.
create_card itself with funds_source: "company_flow". Your own call counts as the collection confirmation, and passing an idempotency_key makes retries attach instead of double-funding. Reusing a key with a different amount, cardholder, or card type is rejected with 409 idempotency_key_reuse; use a fresh key for each new request.
Reusable (multi-use) cards
Company funding works for both card types. Passtype: "multi_use" (optionally with expires_at) to mint a reusable card from the pool: it survives repeated charges and closes when its balance is spent (card.closed with reason balance_exhausted) or its expiry passes. Everything above applies unchanged — one funding, one collection, one card.created carrying the type.
Topping up a reusable card
Raise an existing company-funded multi-use card from the pool withPOST /api/v1/cards/{id}/topups:
amount_cents. The response is 200 with the updated card and the transfer_id, or 202 funding_in_progress (retry with the same Idempotency-Key to attach and collect the result).
Idempotency-Keyis required (400 without one). A replayed key returns the applied result instead of topping up twice; reusing a key with different parameters — including across a card creation and a top-up — is a409 idempotency_key_reuse.- The webhook sequence is the one you already handle:
card_flow.started(withkind: "top_up"and thecard_id) →transfer.completed(your billing anchor, funding is final here) →card.updatedcarrying thetransfer_idas the applied confirmation. - If the card closes while a top-up is in flight, the flow ends with
card_flow.failed(reason: "card_closed",funds_status: "released_to_headroom"): your collection stands and the value offsets that user’s next company-funded allocation.
Releases with multiple fundings
A reusable card can carry several allocations (its mint plus top-ups). When it closes or expires partially spent, the unused remainder is released across those transfers oldest-first — you receive onetransfer.released per funded transfer, and their released_cents sum to exactly the unused remainder. Fully-consumed allocations release nothing. As always, released value stays as that user’s spending power, offsets their next company-funded allocation, and is recoverable.
Recover unused balance
Released value normally finds its way back on its own: the next company-funded card (or top-up) for that user draws on their existing spending power first, and your pool only sends the difference. Nothing to build — it’s automatic, andGET /api/v1/cardholders/{id} reports the current headroomCents so you can reconcile it.
For a user you won’t fund again (offboarding, a one-off purchase that fell through), pull the residual back into your pool with a recovery:
amount_cents to recover the full current headroom (open cards and in-flight claims subtract from it; asking for more is a 422 insufficient_headroom with the available amount). The same primitive is the recover_funds tool on your company MCP connection, and list_recoveries / GET /api/v1/wallet/recoveries track them.
- Live recoveries are fulfilled by the Agentcard team, usually within a business day: the row starts
requested, andrecovery.completedfires when the funds land back in your pool (the deposit also shows up in your balance like any funding). If a recovery can’t be fulfilled it endsrejectedwith afailure_reason, and the value keeps netting as before — money is never lost in between. - Test mode completes instantly against the mock pool, so you can integrate the webhook loop end to end.
- A recovery and a new card can never jointly spend the same headroom: in-flight recoveries are subtracted from netting, and open cards are subtracted from the recovery ceiling.
Balance tools
Available with your company credentials:
Transfers move through public states
pending (substates: awaiting_collection, collection_confirmed, transferring), funded, consumed, reclaimed, and failed (with a failure_reason).
Try it in test mode
The whole loop runs in test mode with no real money: fund the mock pool from the dashboard’s Company balance page (test mode), then runcreate_card with a test credential. The same webhooks fire with livemode: false, the transfer simulates in about 2 seconds, and the card is a mock. Headroom behaves like live too: close a test card unused and the next test card for that user draws on the released value first (a fully-covered funding completes immediately with transferred_cents: 0), and recoveries complete instantly. Integrate the webhook and collection confirmation exactly as you would in production.