Skip to main content
Webhooks push events to your server as they happen, so you don’t have to poll. Register an endpoint, subscribe to the events you care about, and Agentcard delivers a signed JSON payload for each one — a card being used, a transaction settling, a balance running low. Events fired by sandbox activity carry livemode: false; live activity carries livemode: true. A destination belongs to one mode: it’s created in the mode you’re viewing and receives only that mode’s events — register one in each mode to run both.

Register an endpoint

Webhook destinations live in the dashboard: open your organization → WebhooksAdd destination, pick the events, and paste your endpoint URL. Subscribe to All events, a whole family like transaction.*, or individual events — and change the selection any time. Creating a destination reveals its signing secret (prefixed whsec_) — store it, you use it to verify every delivery’s signature. You can reveal it again from the destination’s page, and rotate it there too (for example after a suspected leak). The same page sends test events so you can prove your handler before any real traffic, pauses or deletes the destination, and shows a live view of recent deliveries with their response codes.

The event payload

Every delivery is a POST with this envelope. Dedupe on id — the same event can be delivered more than once.

Verify the signature

Every delivery carries an AgentCard-Signature header so you can confirm it came from us and wasn’t replayed. The header looks like:
v1 is an HMAC-SHA256 of "{t}.{raw_request_body}" keyed with your endpoint secret. Verify it against the raw body (before any JSON parsing), and reject timestamps that are too old to blunt replays.
A legacy X-AgentCard-Signature: sha256=<hex-hmac> header (an HMAC of the body alone, no timestamp) is also sent for backward compatibility. New integrations should verify AgentCard-Signature — the timestamp gives you replay protection.
Respond 2xx to acknowledge. Any other status (or a timeout) is treated as a failure and retried.

Delivery and retries

We attempt each delivery up to 5 times with backoff on any non-2xx response, timeout, or network error. Requests time out after 10 seconds. The destination’s page in the dashboard shows every recent delivery — status, attempt count, the response code we got, and when the next retry is scheduled. An endpoint that keeps failing is eventually disabled; fix your handler, then re-enable the destination from its page.

Event catalog

Subscribe to any of these by name (or "*" / a "family.*" prefix). Events are grouped below by the flow they belong to — each one shows a full example delivery. The envelope is always the same; only data changes shape.

Card events

The lifecycle of a card: issued, balance or status changes, closed.

card.closed

A card was closed. reason tells you why: manual (closed via the API), single_use_spent (a single-use card made its purchase), balance_exhausted (a multi-use card spent its full limit), or provider_closed (closed upstream by the card provider).

card.created

A card was issued — via the API, or minted at the end of a company-funded flow. When the card was company-funded, transfer_id carries the transfer it was minted for so you can correlate it to your collection; it’s null for direct issuance.

card.updated

A card’s balance or status changed: the balance moving after a transaction, or a multi-use card being paused, resumed, or given a new spend limit (status is PAUSED while the card is paused). Not fired when the change is the card closing; that transition is covered by card.closed.

Cardholder and onboarding events

The people you issue cards to — created directly via the API or through a hosted onboarding session.

cardholder.created

A cardholder was created — via POST /api/v1/cardholders, or by a hosted onboarding session that created a brand-new cardholder (claims of an existing one don’t re-fire this).

cardholder.updated

A cardholder’s details changed. updated_fields lists exactly which fields the update touched.

cardholder_onboarding_session.completed

A hosted onboarding session finished. cardholder_id is the resulting cardholder, external_user_id is your id for the user, and state echoes the opaque value you passed when creating the session. claimed_existing_cardholder is true when the session attached to a cardholder already in your org’s books instead of creating a new one.

platform_connect_session.completed

A company finished your hosted platform connect flow. The payload never carries credentials — treat it as a nudge, verify state, and exchange the code from the redirect server-side (or poll the session by id).

Transaction events

The life of a charge: authorized (funds held) → cleared (settled), or declined / voided. balance.low rides along when a charge leaves the card nearly empty.

balance.low

A transaction left the card’s remaining balance at $10 or less (but above zero). Fires alongside the transaction event, so you can top up or warn the user before the next charge declines.

transaction.authorized

A charge was authorized on a card — the funds are held. id is the provider’s transaction id (dedupe transactions on it; it stays stable through clearing), and balance_cents is the card’s remaining balance after the hold.

transaction.cleared

An authorized charge settled. Same shape as transaction.authorized, with status now final.

transaction.declined

A charge was declined — no funds moved. decline_reason says why (only present on declines).

transaction.voided

An authorization was reversed before settling — the held funds return to the card’s balance.

Merchant connection events

Cardholders linking merchant accounts for agent-driven shopping.

merchant.connected

A cardholder linked a merchant account (e.g. DoorDash) — useful to proactively tell the user their connection is live. Fires on live activity only.

Reward events

Tokenback your organization routes to its users. Configure the routed share with rewardShareBps (0 to 10000 basis points of your revenue share) on PATCH /orgs/:orgId/revenue-config.

reward.earned

A settled charge granted tokens to a connected user out of your routed reward share. tokens counts only the tokens your share funded (1 token = 1 cent of credit value), user_id is the user who earned them, and card_id is the card whose settlement triggered the grant.

Company wallet events

The company wallet funding flow, in firing order: card_flow.started (hold your customer) → transfer.approvedtransfer.initiatedtransfer.completed (capture your customer) → card.created. The failure and release paths (transfer.failed, card_flow.failed, transfer.released) and the pool treasury events (wallet.funded, wallet.balance.low) round out the family.

card_flow.failed

The card mint failed after funds had already moved. You keep your capture: funds_status: released_to_headroom means the money is real spending power for that cardholder and automatically nets against their next company-funded card.

card_flow.started

A user’s company-funded card creation reserved funds in your pool — your moment to collect from your customer (company wallet). When ack_required is true, confirm collection via POST /api/v1/wallet/transfers/:id/collection_succeeded before ack_deadline; with auto-approve it’s informational.

transfer.approved

Collection was confirmed for a company-funded card — the pool transfer proceeds. approved_via says how it was confirmed.

transfer.completed

The funds became the user’s spending power — the funding is final from here. Capture your customer’s payment on this event.

transfer.failed

Company funding aborted before the card was created — release your hold on the customer. reason is one of insufficient_org_funds, ack_timeout, ack_denied, or transfer_failed.

transfer.initiated

The pool-to-user transfer was submitted on-chain. tx_hash is the transaction hash when known — null in sandbox and for fully headroom-netted transfers where nothing moves on-chain.

transfer.released

A company-funded card closed with unused balance — you can release or refund your own hold for released_cents. The unused amount stays as the user’s spending power and offsets their next company-funded card.

wallet.balance.low

Your company wallet dropped under the low-balance threshold you configured. Sent at most once per alert window, not on every check — top up before card creations start failing.

wallet.funded

A USDC deposit landed in your company wallet and was booked. amount_cents is the deposit, balance_cents the pool’s on-chain balance after it.

Identity verification events

The identity verification flow. One event follows a connected user’s verification from start to finish — drive your UI from it instead of polling GET /api/v2/kyc.

identity.verification.updated

Fires whenever a connected user’s verification status changes. user_id is the connected user. status is one of awaiting_documents, needs_information, requires_verification, pending, approved, or rejected. When the status is needs_information, required_fields lists exactly what to collect next; when it’s requires_verification, iframe_url is the face-scan URL to show the user (short-lived — always render the freshest one, don’t store it). Non-terminal statuses (and rejected) may also carry reason — an end-user-safe sentence explaining what the review asked for, safe to show verbatim. Statuses are not one-way: a review can send the user back — pending may return to needs_information (a detail didn’t match the document; re-collect and resubmit the listed fields) or to awaiting_documents (unusable images; upload both sides again). Drive your UI from the current status of every event — see when a review sends the user back. A rejected event can additionally carry reason_code: "duplicate_identity": the person is already verified on another Agentcard account. That rejection is not fixed by new documents; the user resolves it by linking the other account from the verification page, and the same verification then reads approved on the next poll. See duplicate documents. To exercise this event end to end with a test-mode credential, simulate an outcome — the simulated verdict fires the same event (with livemode: false) to your test-mode endpoints.
When more details are needed, the same event carries the fields to collect — and, when the review asked for a correction, a reason to show the user:
And when the user is ready for the face scan, it carries the URL to show them:
A rejection with a machine-readable class carries reason_code: