Skip to main content
This quickstart is for a product with no frontend: a CLI, a backend agent, an email flow, anything that can deliver a URL and make HTTP calls. You create a wallet link and hand the URL to the user however you like; we host the page it opens. Then your server calls POST /buy itself and gets the cart back as data, with a hash it echoes to confirm. By the end, a test user has added a card on the hosted wallet, and your server has run a real purchase conversation at Amazon right up to the sandbox wall. About fifteen minutes, all in sandbox, so no email is sent, no money moves, and the verification code is always 111111. If you do have a frontend, the web app quickstart is the default; Choose a quickstart has the rest.

Prerequisites

  • An Agentcard organization with sandbox credentials. In the dashboard, open Settings → Developers → Credentials; if there’s no client yet, Implement Agentcard in the same menu creates one.
  • curl, Node 18+, or Python 3.8+ with requests, whichever you prefer. Every call on this page is shown in all three.
1

Get your sandbox credentials

Your sandbox client_id and client_secret are in the dashboard under Settings → Developers → Credentials. Sandbox or production follows the credential, not the URL: there is one API at api.agentcard.sh, and a sandbox credential makes every call on this page a sandbox call, so nothing is emailed, nothing is texted, and no money moves.Exchange the credentials for a bearer token. The request is form-encoded, per OAuth:
The token comes back with its lifetime:
Output
Export it as ORG_TOKEN; it’s the Authorization: Bearer on every server call below. It lasts an hour and there’s no refresh token, so when it expires you request a new one the same way.While you have the token, give the last step something to read: create a sandbox webhook destination. Events are only recorded when your organization has a destination to deliver them to, so a destination created after the fact shows nothing. Skip this if you already have one in sandbox.
url is any public https address you can watch: your own endpoint, or a request-inspection service while you test. It doesn’t have to answer 2xx for this walkthrough, because you’ll read the deliveries in the dashboard. The response carries the signing secret once, and livemode: false tells you the token was a sandbox one:
Output
You can do the same in the dashboard under Settings → Developers → Webhooks with Add destination, with the Live mode switch off so the destination is a sandbox one. Destinations belong to one mode and receive only that mode’s events.
2

Connect a test user

Start a connection for a test user. In sandbox nothing is actually sent, so any email works, and sandbox identities are isolated: connecting testuser@example.com here can never touch a real account with that address.
Send phone instead of email to connect by text message; the call takes exactly one of the two. external_user_id is optional: it’s your own id for the user, and it comes back on webhooks so you can match them up. The response is the attempt, good for ten minutes:
Output
Verify it with the sandbox code, which is always 111111:
The response is the connection: the user’s id, an access_token, and a refresh_token:
Output
Store all three. user.id is what you create wallet links for. The access_token is the user’s connection token: it’s the bearer the user’s agent buys with in step 4, over MCP or POST /buy, so export it as USER_ACCESS_TOKEN now. It lasts an hour; the refresh_token rotates it (POST /api/v2/connect/refresh), and each refresh returns a new pair and invalidates the old one. Your first webhook, connection.created, fires here.Then record the user’s authorization. Consent is what makes the user’s wallet available to your product: wallet links can’t be created for a user until it’s on file, and the call is idempotent, so a retry updates the record instead of duplicating it. In your product the wallet shows the user a consent screen; in this walkthrough you record it directly:
Output
That’s the whole server side of connecting a user. Connect users covers the same calls with production in mind: where each credential lives, refreshing sessions, and the errors you can get back.
3

Hand the user a hosted wallet link

The wallet for this shape is a hosted page we run, reached through a link your server creates:
Output
Deliver the url any way your product can: print it in the terminal, put it in an email, drop it in a chat. For this walkthrough, open it in your browser. The user gets the full wallet: add a card, see their cards, approve a payment. Add a card with the test number 4242 4242 4242 4242, any future expiry, any CVC.Two webhooks go out here: wallet_link.opened the first time the link is opened, and connected_card.updated when the card is added. Don’t poll the link for status; the webhooks are the record of what actually happened.A link belongs to one user, expires after 15 minutes by default (expires_in accepts 60 to 86400 seconds), and can be opened up to 20 times within that window, so the user can open it again without asking for a new one. Link previews and unfurl bots never consume an open, because the page only exchanges the link when a real browser loads it. When the user needs the wallet again later, create a fresh link; they’re cheap and there’s no limit on how many you create.The same link can also open straight onto a pay sheet for one specific charge, by appending merchant (a display name) and amount (in cents) to the URL. That’s the user approving a charge your product already priced, with a card from their wallet. What comes next is different: your server asking the agent to go buy something at a merchant, which is buy.
4

Let the agent buy

With no frontend, your server is the agent’s hands: it calls POST /buy directly with the user’s connection token from step 2, shows the reply wherever your product talks to the user, and confirms with the cart’s hash.Your server sends what the user wants as plain text. The bearer is the user’s token from step 2, so everything that follows runs as that user: the agent searches the merchant, asks follow-up questions, and stops each turn at needs_input until it has enough to build a cart.
Give the client a timeout of at least 120 seconds: each turn runs against a live merchant. The first turn usually asks a question. Yours will differ, because sandbox conversations are live, not scripted:
Output
needs_input is not an error; it’s how a purchase progresses. Show the reply to the user and send their answer back as the next ask on the same conversation_id:
Keep answering until the response carries a cart with a totalCents and a hash. That’s the agent showing the exact all-in price before anything happens:
Output
Confirm by echoing the cart’s hash, not the word “yes”. The hash binds the approval to exactly this cart at exactly this price: if anything drifted since the cart was shown, the confirm returns a 409 with the fresh carts instead of charging something the user never saw.
The hash verifies, the confirm gate passes, and then sandbox refuses the one thing it exists to refuse. Before any money is reserved or any card is created, the checkout denies with the sandbox wall:
Output
That refusal is the finish line, not a failure. You ran the whole purchase contract: an ask, a real cart at a real merchant with an exact total, and a confirmation that verified before anything moved. Sandbox stops at the money on purpose, because its test cards can never charge a real merchant, so no sandbox call ever places an order, and retrying this confirm on a sandbox connection always returns the same refusal. On a production connection this exact confirm is the one that places the order: Agentcard reserves the amount, issues a one-time card funded from the wallet, pays the merchant, and returns the receipt.
Branch on status, never on the reply text: the prose varies run to run, the status and the cart don’t. The Purchase API page has the full envelope, multi-merchant confirms, and the rules a production integration needs.
5

See what happened

Open the dashboard with the Live mode switch off, go to Settings → Developers → Webhooks, pick the sandbox destination from step 1, and open its Deliveries tab. Every event from this walkthrough is there with its payload and the response code your destination answered. From the terminal, agent-cards companies webhooks deliveries ENDPOINT_ID lists the same rows.Every event arrives in the same envelope, and livemode is false because the credential behind it was a sandbox one:
connection.created
What you’ll find, in order: connection.created from step 2, wallet_link.opened the first time the wallet opened from its link, and connected_card.updated when the card was added. On the company-funded quickstart it’s cardholder.created from step 2 instead. Nothing arrived from the purchase, and that’s correct: sandbox stopped the confirm before a card existed, so the transaction.* events a live order produces never fired. To rehearse those in sandbox, test_charge on the organization MCP server simulates a full charge against a sandbox card and sends transaction.authorized, transaction.cleared, and card.closed; Test in sandbox has every knob.In production your server listens for these instead of reading them in the dashboard. The SDK callbacks and the conversation are UI signals; webhooks are the record. Deliveries are at least once, so deduplicate on id, and verify the AgentCard-Signature header against the raw request body: Webhooks shows the scheme and lists every event.

Next steps

You connected a user, handed them a wallet, they added a card, and your server ran a real purchase conversation to the confirm. In production the same five steps are your integration; only the credentials change.

Hosted link

Link semantics in full, delivery options, and the rules for your agent.

Purchase API

The full envelope, multi-merchant confirms, how the money moves, and the rules a production caller needs.

Webhooks

Verify signatures and handle every event on your server instead of reading them in the dashboard.

Go live

Subscribe, switch to production credentials, and run the loop once with real money.