Every authenticated request in a company integration goes through the Agentcard MCP server, with the user’s OAuth access token:
It’s a standard MCP server over Streamable HTTP: initialize → notifications/initialized → tools/list → tools/call, echoing the Mcp-Session-Id header. Any spec-compliant MCP client handles this for you — point it at the URL with the token from the OAuth flow and the tools load automatically.
Register every tool tools/list returns instead of hardcoding names. Tools Agentcard ships later then appear in your integration with zero code change. If you ever need a denylist, make it config-driven and default it to empty.
Two groups are hidden from the default tools/list. Every gated tool remains callable by name; gating trims what agents discover, not what dispatches:
- Shopping granulars: the
buy_* flow tools. The conversational buy tool drives them server-side, so agents only need buy.
- Advanced tools: niche analytics, error-triggered one-offs, and companions of an advertised tool:
list_all_transactions, list_transactions_by_payment_method, submit_funding_profile, complete_kyc_transfer, list_codes, set_default_payment_method, remove_payment_method, cancel_plan, and update_settings. A typical agent never needs to discover these to complete a core flow; each one’s page carries an “Advanced tool” note.
Browse the full surface, gated tools included, with agent-cards api tools --all.
Two conventions worth wiring into your client up front:
- Result fields are camelCase (
cardId, spendLimitCents, cardStatus: "OPEN"), while request parameters and the REST v2 API are snake_case (card_id, spend_limit_cents). Don’t assume one casing across both directions — send snake_case, read camelCase.
isError flags rejected calls. A rejected call (a validation failure, a missing or invalid resource, an operation that didn’t happen) sets isError: true, with the reason in the text and a status discriminator in structuredContent. A successful call leaves isError unset, including one that returns a modeled next step like approval_required or kyc_required (an outcome to act on, not a failure). This flag is still rolling out across the tool surface: the card-lifecycle tools (pause_card, resume_card, update_card_limit) set it today and the rest are following. So treat isError: true as authoritative wherever you see it, but until the rollout finishes don’t read its absence as a guaranteed success on every tool: branch on isError when present, then on the status discriminator in structuredContent.
Approval gates
Sensitive calls can pause for explicit approval. When create_card or get_card_details returns a status of approval_required with an approval_id, nothing has happened yet — resolve it with approve_request. Auto-approve when the user just asked for the action; confirm with them first before revealing card credentials.
get_card_details returns the full card number and CVV. Never write these to logs, error reports, or analytics.
Whether those cards are live or test is set by the OAuth client your app connected with: a sandbox client mints test cards (no real charge), a production client mints live cards (which requires an active subscription). See Production.
Funding and KYC
Cards are funded from the user’s Agentcard wallet. If create_card reports that KYC or wallet funding is needed, relay the message to the user — they complete it in their own account — then retry. If a deposit is still confirming, wait the suggested interval and retry.
One wallet across apps
The user has one wallet, and every connected app sees it. list_cards returns a wallet array: every live card the user holds, personal and company-funded, whatever app minted it, each row tagged with:
access: owned (your connection minted it; full control) or connected (visible, but actions need the user’s approval or are read-only). This field is server-authoritative and relative to your session: key any read-only affordances off it, never off heuristics.
source: where the card came from, { kind: "personal" | "company", app, appName }, plus companyId/companyName on company-funded rows.
The legacy cards and connectedAccounts fields keep their pre-shared-wallet semantics (cards is only what your connection created), so nothing built against them breaks.
Acting on another app’s card
Card actions on a personal card another app minted don’t fail; they pause for the user’s approval. The five gated verbs are get_card_details, close_card, pause_card, resume_card, and update_card_limit:
- Call the tool as usual. If the card is
connected, it returns status: "approval_required" with an approvalId (kind: "cross_app", HTTP 202 on the REST surface); nothing has happened yet.
- The user is emailed an approve/deny link. This gate is resolved by that link, not by
approve_request.
- Once they approve, retry the same tool with
approval_id set.
Approvals are single-use, expire after 10 minutes, and bind to the exact card, action, and requesting app (and, for update_card_limit, the exact new limit). A consumed, expired, or mismatched approval returns approval_not_valid; retry without approval_id for a fresh one.
Company-funded cards stay read-only from other apps (managed_by_organization): the owning company manages them via its own surface.
Card isolation
A token from your client can only act on the cards your app created for that user (plus cross-app actions the user explicitly approves, above). It can’t touch another user’s anything. A brand-new connection listing zero cards in the legacy cards field is expected, not a bug.
Browse the sidebar for every tool — each has its own page with parameters, returns, and an example call. The ones you’ll use most: create_card, get_card_details, list_cards, get_card_balance, close_card, and approve_request.