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

# Integrate the Purchase API into an iMessage agent

> Learn how to make an iMessage agent shop at real merchants through Agentcard's Purchase API and pay with the user's vaulted card.

This guide takes the iMessage agent from [Create an iMessage agent and connect the Vault](/guides/create-an-imessage-agent-and-connect-the-vault) and teaches it to buy things. The user texts what they want; the agent finds it at Amazon, Walmart, Target, Best Buy, DoorDash or another merchant through [Agentcard's Purchase API](/vault/integrations/ecommerce-apis/purchase-api), shows the cart, and places the order when the user says so, paying with the card they stored in the Vault.

The first guide covered the plumbing: Linq, Vercel eve, Redis, Agentcard credentials and webhooks. This one is about the code that makes the agent shop, and every part of it is in the repo you copy.

What you end up with: a phone number where a user texts "buy me a 20 oz bag of whole bean coffee from Amazon", gets a product, a picture and a price back within a minute, says yes, taps an approval on their phone, and reads "Order placed" in the thread twenty seconds later, with nothing else to type.

## Before you start

<Note>
  Testing this end to end needs production credentials. The sandbox stops at the confirm (`sandbox_mode`): no approval link, no webhook, no order. See [Going to production](#going-to-production).
</Note>

Finish the [first guide](/guides/create-an-imessage-agent-and-connect-the-vault). Everything it set up is reused here: the Vercel account and CLI, the Linq sandbox, the Agentcard organization and its `client_id` / `client_secret`, the Redis database. This agent is that agent plus a shopping loop.

## Tools

<CardGroup cols={3}>
  <Card title="Linq" href="https://linqapp.com" img="https://mintcdn.com/agentcard/E3IhMy00FMCdDPgu/images/logos/linq.png?fit=max&auto=format&n=E3IhMy00FMCdDPgu&q=85&s=28208e064c93fe3d5beeed5d3faeae8b" width="128" height="128" data-path="images/logos/linq.png" />

  <Card title="Agentcard Purchase API" href="/vault/integrations/ecommerce-apis/purchase-api" img="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/logos/agentcard.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=65d1dac5a31a8025bfb126178b459052" width="512" height="512" data-path="images/logos/agentcard.png" />

  <Card title="Vercel eve" href="https://vercel.com/docs/eve" img="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/logos/vercel.svg?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=d7c44df28b4c248f6418c61887907ad5" width="128" height="128" data-path="images/logos/vercel.svg" />
</CardGroup>

## How it works

A purchase runs as one user, so the agent needs a token for that user. The Vault link from the first guide is the sign-up: the user stores a card with a passkey, and the agent exchanges the linked session for the user's connection tokens. No code, no account. Shopping is then a loop on one endpoint, `POST /buy`: an `ask` builds or refines a cart, a `confirm` with the cart's `hash` places it. The user's vaulted card pays, after an approval they tap on their phone, and a webhook tells the agent the moment they did.

| Text from the user | What the agent does |
| - | - |
| "buy me coffee from Amazon" | New user: `create_vault_link` texts an open Vault link. |
| (stores a card with Face ID) | `vault.session_linked` reaches the webhook; the agent exchanges the session for the user's tokens, keyed by phone, and tells them they are set. |
| (same ask, now connected) | `buy` with `ask`: the Purchase API searches the merchant and returns products, each with its page `url` and `image_url`, or a question. |
| "show me" | `send_image` with the product's `image_url`: the picture lands as a photo in the thread. |
| "yes, add it" | `buy` with the answer as the next `ask`: a `cart` with a `hash` comes back. The agent shows items and total. |
| "place it" | `list_cards` first: a user with a card in the Vault gets no link. Then `buy` with `confirm: <hash>` and `payment_source: "vault"`. The confirm pauses with an `approval_url`, which the tool texts as its own bubble. |
| (taps the link, approves with Face ID) | Agentcard delivers `checkout_authorization.approved` to the agent's webhook, the agent wakes the conversation, repeats the confirm and reports the order. The user never says "done". |

In the sandbox everything runs against the real merchant up to the confirm, which is declined with `decline_code: "sandbox_mode"` by design; the last two rows of the table only happen in production.

## 1. Copy the blueprint and deploy it

The repo is [tiny-agent-company/purchase-imessage-agent](https://github.com/tiny-agent-company/purchase-imessage-agent), created from the first guide's repo as a template.

```bash theme={null}
git clone https://github.com/tiny-agent-company/purchase-imessage-agent my-shopping-agent
cd my-shopping-agent
npm install
cp .env.example .env.local
```

Deploy it exactly as in the first guide ([credentials](/guides/create-an-imessage-agent-and-connect-the-vault#2-get-your-agentcard-credentials), [Vercel](/guides/create-an-imessage-agent-and-connect-the-vault#4-deploy-to-vercel), [Linq](/guides/create-an-imessage-agent-and-connect-the-vault#5-point-linq-at-the-deployment), [Agentcard webhook](/guides/create-an-imessage-agent-and-connect-the-vault#6-point-agentcard-at-the-deployment)), with three differences:

* Two more variables: `AGENTCARD_MODE=sandbox` (the connect code is `111111`, confirms end in `sandbox_mode`) and `STORE_PREFIX=purchase` (several agents can share one Redis).
* The Agentcard webhook endpoint subscribes to the approval events too: `["vault.card_stored", "vault.session_linked", "checkout_authorization.approved", "checkout_authorization.declined", "checkout_authorization.expired"]`. One endpoint per deployment.
* One Linq sandbox line serves one agent at a time (Linq issues one sandbox per phone number and per email domain). To run this agent on the first guide's line, move its subscription instead of creating one; the signing secret stays the same, so the same `LINQ_WEBHOOK_SECRET` goes on this project:

```bash theme={null}
curl -X PUT https://api.linqapp.com/api/partner/v3/webhook-subscriptions/$SUBSCRIPTION_ID \
  -H "Authorization: Bearer $LINQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_url": "https://my-shopping-agent.vercel.app/eve/v1/linq"}'
```

`GET /api/partner/v3/webhook-subscriptions` lists your subscriptions with their ids.

The rest of this guide walks through the new code, in the order a purchase runs.

## 2. Connect users without login

Every `/buy` call runs as the user, with a user token. The first guide's open Vault link already makes the user prove themselves on the vault page and store a card; `POST /api/v2/vault_sessions/{id}/exchange` turns that finished link into the user's connection tokens, once, with the organization token. The user never receives a code and never creates an account.

The order of events, all in `agent/channels/agentcard.ts`:

1. A new user texts. `buy` returns `not_connected`, the agent calls `create_vault_link`, and the tool creates an open session (`POST /api/v2/vault_sessions` with `{}`), remembers `vault session → eve session`, and texts the `url` alone.
2. The user stores a card. Agentcard delivers `vault.session_linked` (and, seconds later, `vault.card_stored`) with the `vault_session_id`.
3. The webhook handler in `agent/channels/agentcard.ts` exchanges the session and stores the pair under the phone:

```text theme={null}
const c = await agentcard<{ access_token: string; refresh_token: string; expires_in: number; user: { id: string } }>(
  "POST",
  `/api/v2/vault_sessions/${vaultSessionId}/exchange`,
);
await saveConnection(phone, {
  user_id: c.user.id,
  access_token: c.access_token,
  refresh_token: c.refresh_token,
  expires_at: Date.now() + c.expires_in * 1000,
});
```

That is `connectFromVaultSession` in `agent/lib/user.ts`. The response is single use: one session, one pair, and a second call is refused with `410 already_exchanged`, so the handler marks the session in Redis before calling and lets the two link events race for it. From then on `userToken(phone)` hands every tool a live token and rotates it through `POST /api/v2/connect/refresh` when the access token is within a minute of expiring; the exchange is never called again for that user.

The `[Agentcard]` message the webhook sends into the conversation says whether the exchange connected the user, so the agent's next sentence is right: "Your card is set up, want me to place it?"

### When the user already has an Agentcard account

The link proves access to the vault, not to the account. A user who opens the link and signs in to an Agentcard account that already existed gets `403 account_verification_required` on the exchange, and the code flow is the way in: `connect/start` texts a six-digit code to the number the conversation is with, `connect/verify` exchanges it for the same token pair, `connect/consent` records the consent. `agent/tools/connect_user.ts`:

```text theme={null}
const attempt = await agentcard<{ id: string; channel: string; expires_at: string }>(
  "POST",
  "/api/v2/connect/start",
  email ? { email } : { phone }, // email only when the user says the text never came
);
await rememberConnectAttempt(phone, attempt.id); // ten minutes, keyed by phone
```

`agent/tools/verify_code.ts` finishes it with `connect/verify` and `saveConnection`. The handler's `[Agentcard]` message names this case, and the instructions tell the agent to call `connect_user` only then. In the sandbox no text is sent and the code is always `111111`; on real phones some carriers drop the text, so `connect_user` accepts an `email` too.

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/imessage-connect-code.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=2f7aadfb889934450af06075d23672d1" alt="The fallback only: the code Agentcard texts from its own number to a user who already had an account" width="1280" height="177" data-path="images/guides/purchase-imessage/imessage-connect-code.png" />

The fallback only: the code Agentcard texts from its own number to a user who already had an account

## 3. Shop with `/buy` as the user

The whole shopping loop is one call, made with the user's token rather than the organization's. `agent/tools/buy.ts`:

```text theme={null}
const conn = await userToken(phone);
if (!conn) return { status: "not_connected", next: "Call connect_user, then verify_code." };

const body: Record<string, unknown> = {};
if (conversation_id) body.conversation_id = conversation_id;
if (confirm) {
  body.confirm = confirm;            // the cart hash from a previous turn
  body.payment_source = "vault";     // the user's own card, approved with their passkey
} else {
  body.ask = ask;                    // what the user said, in their words
}
const r = await agentcardAs<BuyResponse>(conn.access_token, "POST", "/buy", body);
```

A turn can take up to two minutes while the Purchase API works the merchant, so `agentcardAs` uses a 120-second timeout. The response's `status` drives the agent: `needs_input` with a `reply` to relay and maybe a `cart`, `order_placed` with an `order_id`, or `declined` with a `decline_code`. A `409` means the cart moved since that hash was issued; the body carries the fresh cart, and the tool returns it as `cart_changed` so the agent shows it again instead of retrying blind.

The tool hands the model a small, typed view of the response rather than the raw JSON:

```text theme={null}
return {
  status: r.status,
  conversation_id: r.conversation_id,
  reply: r.reply,
  cart: r.cart ? summarize(r.cart) : null,   // lines as "1 × name ($24.99)", fees, total, hash
  products,                                  // name, price, url, image_url
  approval_link_sent,
  decline_code: r.decline_code ?? undefined,
  order_id: r.order_id ?? undefined,
  payment_source: r.payment_source ?? undefined,
};
```

Every product in `catalog.items` carries the merchant's identifier as `id` and its photo as `image_url`. For the retail merchants the `id` is the product page URL (for Amazon, the `/dp/` URL); for DoorDash it is an opaque item id, so the tool passes it on as `url` only when it starts with `https://` and never sends anything else as a link. `catalog` stays on the conversation for fifteen minutes after the search and is `null` after that, so the tool keeps the last one per conversation in Redis for a day: the agent still needs the links and pictures when the user asks "which one was that?" the next morning. See [the catalog object](/api-reference/purchases/overview#the-catalog-object) for the fields.

## 4. Send links and pictures through Linq

iMessage renders a URL that stands alone in a message as a preview card, and mangles one with words glued to it. So the model never sees a URL: the tools send them. `agent/tools/send_link.ts` texts a product `url` from a tool result as its own bubble, and refuses anything that did not come from one.

Pictures are a Linq `media` part with the image's https URL; Linq fetches it and delivers a photo, no upload step. `agent/lib/linq.ts`:

```text theme={null}
export async function sendMedia(to: string, url: string): Promise<void> {
  await linq("POST", "/chats", {
    from: await senderNumber(),
    to: [to],
    message: { parts: [{ type: "media", url }] },
  });
}
```

That is all `send_image` does with a product's `image_url`. A `text` part and a `media` part can share one message, but the blueprint sends the picture alone so the agent's sentence never gets attached to it. For an image you hold as bytes rather than a URL, create an attachment first (`POST /api/partner/v3/attachments` returns an `upload_url` and an `attachment_id`) and send `{ "type": "media", "attachment_id": "…" }` instead.

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/imessage-photo-and-link.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=7fbf8f15766fe0de8d0d5743dade5c8a" alt="send_link and send_image: the merchant page as its own bubble, the photo as its own message, then the agent's sentence" width="1280" height="711" data-path="images/guides/purchase-imessage/imessage-photo-and-link.png" />

send\_link and send\_image: the merchant page as its own bubble, the photo as its own message, then the agent's sentence

## 5. Check the Vault before sending a link

A returning user already has a card, and asking them to store another is the fastest way to lose them. For a connected user, before any Vault link, the agent lists their cards, as [Checking for stored cards](/vault/checking-for-stored-cards) describes. `agent/tools/list_cards.ts`:

```text theme={null}
const conn = await userToken(await phoneOf(ctx));
const r = await agentcard<{ data?: VaultCard[] }>(
  "GET",
  `/api/v2/vault_cards?user_id=${encodeURIComponent(conn.user_id)}`,
);
```

`create_vault_link` does the same check itself and refuses to send a link to a user who has cards, unless the agent passes `force` because the user asked to add one. When it does send one, the Vault session is bound to the connected user, so the card lands on the account `/buy` runs as:

```text theme={null}
const session = await agentcard<VaultSession>("POST", "/api/v2/vault_sessions", { user_id: conn.user_id });
await rememberVaultSession(session.id, ctx.session.id); // for the card-stored webhook, as in the first guide
await sendText(phone, session.url);                      // the link, alone in its own bubble
```

For a user who is not connected yet there is nothing to list: the open session (no `user_id`) is the sign-up itself, and step 2 turns it into the connection.

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/agentcard-vault-cards.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=6620bd78c20da44830f99ddc32f8ebeb" alt="Vault → Stored cards in the dashboard: the same list list_cards reads per user" width="1280" height="900" data-path="images/guides/purchase-imessage/agentcard-vault-cards.png" />

Vault → Stored cards in the dashboard: the same list list\_cards reads per user

## 6. Place the order and learn of the approval

The confirm is where the two halves meet. `buy` with `confirm` and `payment_source: "vault"` does not place anything yet: it comes back `declined` with `decline_code: "vault_approval_required"` and an `approval_url`. The tool texts that URL alone, and remembers which conversation is waiting on it:

```text theme={null}
if (r.approval_url) {
  await sendText(phone, r.approval_url);
  approvalLinkSent = true;
  const id = new URL(r.approval_url).searchParams.get("id"); // cauth_…
  if (id && confirm) {
    await rememberApproval(id, { eveSessionId: ctx.session.id, conversationId: r.conversation_id, hash: confirm });
  }
}
```

The user taps the link and approves with Face ID or Touch ID. Two things happen at once on Agentcard's side: the order is placed against the approval, and `checkout_authorization.approved` is delivered to the same webhook endpoint that receives the Vault events:

```json theme={null}
{
  "type": "checkout_authorization.approved",
  "livemode": true,
  "data": {
    "authorization_id": "cauth_80a08296fda037b12d62c351",
    "user_id": "usr_…",
    "merchant": "Amazon, Walmart, Target & Best Buy",
    "amount": 3644,
    "amount_display": "$36.44",
    "psp": "stripe",
    "mode": "token"
  }
}
```

`agent/channels/agentcard.ts` looks the authorization up and wakes the paused conversation, the same `attachSession(...).send` the first guide uses for a stored card:

```text theme={null}
if (event.type.startsWith("checkout_authorization.")) {
  const authId = String(event.data.authorization_id ?? "");
  const pending = authId ? await approvalFor(authId) : null;
  if (!pending) return new Response("ok"); // not one of this agent's confirms
  const text = approvalNote(event, pending);
  if (text) waitUntil(attachSession(pending.eveSessionId).send(text, { auth: WEBHOOK_AUTH }));
  return new Response("ok");
}
```

The note is a plain instruction the model acts on without asking the user anything: for `approved`, "call `buy` now with confirm `<hash>` on conversation `<id>`, then tell them the result"; `declined` and `expired` become one sentence to the user. The amount on the event is the approval ceiling, not necessarily the charge: tax and shipping settle later, and [`order.placed`](/webhooks/orders/order-placed) carries the final figure.

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/imessage-approval-to-order.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=20d4553faec38f87ef5e28f93c6496df" alt="The confirm, the approval link alone in its own bubble, and the order reported after the webhook, with nothing typed in between" width="1280" height="449" data-path="images/guides/purchase-imessage/imessage-approval-to-order.png" />

The confirm, the approval link alone in its own bubble, and the order reported after the webhook, with nothing typed in between

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/agentcard-webhook-deliveries.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=82f1aacc91eb26a5a69926bd79761461" alt="Settings → Developers → Webhooks → Deliveries: checkout_authorization.approved delivered to the agent, 200" width="1280" height="900" data-path="images/guides/purchase-imessage/agentcard-webhook-deliveries.png" />

Settings → Developers → Webhooks → Deliveries: checkout\_authorization.approved delivered to the agent, 200

Two details make this path reliable:

The confirm may arrive while Agentcard is still placing. The approval starts the placement on Agentcard's side, and the agent's own confirm a second later can find that placement in flight. That confirm comes back with `decline_code: "in_progress"`: not a failure, a "wait". The tool waits, reads the conversation back, and reports the order that belongs to this checkout (`last_checkout.order_id`; orders are listed oldest first, so the first entry may be an earlier purchase):

```text theme={null}
const inProgress = r.decline_code === "in_progress" || (r.status === "error" && !r.decline_code);
if (confirm && inProgress) {
  await new Promise((res) => setTimeout(res, 12_000));
  const conv = await agentcardAs<Conversation>(conn.access_token, "GET", `/buy/conversations/${r.conversation_id}`);
  const orderId = conv.last_checkout?.order_id ?? conv.orders?.filter((o) => o.status !== "failed" && o.status !== "cancelled").at(-1)?.order_id;
  if (orderId) return { status: "order_placed", conversation_id: r.conversation_id, order_id: orderId, cart: summarize(r.cart) };
}
```

A turn started by a webhook has no phone number. The Linq channel puts the sender's number in the turn's auth context; a turn that a webhook woke has no Linq auth at all, so `phoneOf(ctx)` would find nothing. `agent/lib/user.ts` remembers the phone per eve session on every text and reads it back on those turns:

```text theme={null}
export async function phoneOf(ctx): Promise<string> {
  const phone = ctx.session.auth.current?.attributes?.user_name;
  if (typeof phone === "string" && phone) {
    await rememberPhone(ctx.session.id, phone).catch(() => undefined);
    return phone;
  }
  const remembered = await phoneForSession(ctx.session.id);
  if (remembered) return remembered;
  throw new Error("No phone number on this conversation");
}
```

Without it, the first thing the agent does after the approval, the confirm, fails on the one turn that matters.

## 7. Text it

From the phone you activated the Linq sandbox with:

```text theme={null}
buy me a 20 oz bag of whole bean coffee from amazon
```

The agent connects you first (sandbox: the code is `111111`), then names a product and a price within about thirty seconds. Ask for a picture and the photo arrives as its own message. Say yes, and it shows the cart with the total and your address, then asks to place it.

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/imessage-ask-to-cart.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=5a7205635fc120fdcfaa9a3581d60c8b" alt="Connect by code, then the Purchase API finds the product and builds the cart" width="1280" height="578" data-path="images/guides/purchase-imessage/imessage-ask-to-cart.png" />

Connect by code, then the Purchase API finds the product and builds the cart

In the sandbox the confirm comes back `sandbox_mode` and the agent explains that the order stops there by design:

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/imessage-sandbox-confirm.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=f3de3c43a3e4c238dac618d7c88e6409" alt="The sandbox confirm: the loop ran for real up to the payment" width="1280" height="159" data-path="images/guides/purchase-imessage/imessage-sandbox-confirm.png" />

The sandbox confirm: the loop ran for real up to the payment

Every conversation is in the Agentcard dashboard under **Agents → Purchase → Conversations**: what the user asked, what the Purchase API answered, the cart on the table, and each `/buy` request with its status.

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/agentcard-vault-approvals.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=c36c69ed64f8e884fc76d8a00ed94e9a" alt="Vault → Checkout approvals: every approval link the agent sent, and whether the user approved it" width="1280" height="900" data-path="images/guides/purchase-imessage/agentcard-vault-approvals.png" />

Vault → Checkout approvals: every approval link the agent sent, and whether the user approved it

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/agentcard-purchase-conversations.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=e4fb7fca2e61206381ba28701723a3ef" alt="Agents → Purchase → Conversations: the same conversation from Agentcard's side" width="1280" height="900" data-path="images/guides/purchase-imessage/agentcard-purchase-conversations.png" />

Agents → Purchase → Conversations: the same conversation from Agentcard's side

## Going to production

This is the step that makes the purchase real: the approval link, the webhook and the order only exist with Live credentials. Sandbox and production are two sets of credentials on the same organization and two webhook endpoints. No code changes.

1. In the Agentcard dashboard, open **Settings → Developers → Credentials** and switch **Live** on. The first time, the dashboard asks you to register your production app: a name, redirect URIs empty. You get a production **Client ID** and **Production secret**.

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/agentcard-register-production-app.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=753f6e3ecd5b9f782df55e8a054870b0" alt="Register your app: the production client is created here, once" width="1280" height="900" data-path="images/guides/purchase-imessage/agentcard-register-production-app.png" />

Register your app: the production client is created here, once

<img src="https://mintcdn.com/agentcard/h4dHYVJUFjVn9a7-/images/guides/purchase-imessage/agentcard-live-credentials.png?fit=max&auto=format&n=h4dHYVJUFjVn9a7-&q=85&s=45e15ecd8009b1a8dafe50ac4e91e9cd" alt="Settings → Developers → Credentials in Live mode: the production client id and secret" width="1280" height="900" data-path="images/guides/purchase-imessage/agentcard-live-credentials.png" />

Settings → Developers → Credentials in Live mode: the production client id and secret

2. Exchange them for a production token and register a production webhook endpoint with the same five events. Endpoints belong to a mode: the token decides which one you create.

3. Replace the four Agentcard variables on Vercel (`AGENTCARD_CLIENT_ID`, `AGENTCARD_CLIENT_SECRET`, `AGENTCARD_WEBHOOK_SECRET`, `AGENTCARD_MODE=production`) and deploy.

<Note>
  `eve deploy` rewrites `.env.local` from the Vercel project after each deploy, quoting every value. If you copy values out of that file into `vercel env add`, strip the quotes: a quoted client id is rejected as `invalid_client`, and a quoted webhook secret fails every signature check.
</Note>

4. To let anyone text the line, upgrade the Linq sandbox. The line, the token and the webhook stay.

In production the connect code arrives as a real text from Agentcard, the Vault link stores a real card, and the confirm pauses on the approval link. Measured on a live run: the tap, the webhook, the placement and "Order placed" in the thread took twenty-two seconds, with nothing typed after the tap.

## If nothing comes back

Everything in the [first guide's checklist](/guides/create-an-imessage-agent-and-connect-the-vault#if-nothing-comes-back) applies. Additions:

* The card was stored but the agent still says the user is not connected: the exchange did not run or was refused. `vercel logs` shows the handler's attempt; `409 not_linked` means the event raced the link (the next event retries), `410 already_exchanged` means the tokens were minted on an earlier call and Redis lost them (send a new link), `403 account_verification_required` means the user signed in to an existing account and the agent must fall back to the code.
* The agent asks for a code: only right when the webhook said the account already existed. `verify_code` returning `no_attempt` or `wrong_code` means the attempt expired (ten minutes) or the digits were wrong; in the sandbox only `111111` verifies. If the text never arrives, the agent asks for an email and sends the code there.
* `buy` returns `not_connected` on every turn: the connection was revoked and the store forgot it. The agent reconnects with a new code.
* The agent sends a Vault link to someone who already has a card: it skipped `list_cards`. The `buy` reply can say "need a card on file" when the Purchase agent has not looked at the vault; the tool result, not that prose, decides.
* After the approval the agent says the store could not place the order: the confirm hit `in_progress` and the re-read found no order. Read `GET /buy/conversations/{id}` with the user's token yourself: `orders` and `last_checkout` say what happened, and the authorization stays approved for fifteen minutes.
* After the approval the agent says nothing at all: the webhook did not reach it. Check the endpoint's deliveries in the dashboard, and that the turn it woke had a phone to answer to (`No phone number on this conversation` in `vercel logs` means the session's phone was never remembered).
* The approval link opens the Vault's sign-in page instead of the approval: something was glued onto the URL. In the blueprint no URL passes through the model; keep it that way when you change the instructions.
* A product link opens a 404: the URL was composed instead of taken from the Purchase API. `send_link` only accepts a URL from a tool result; keep the rule that a link is never typed into a reply.

## Where to go next

* [Agentcard's Purchase API](/vault/integrations/ecommerce-apis/purchase-api) for the full loop: addresses as data, price changes, tracking retail orders, multi-cart confirms.
* [The purchase object](/api-reference/purchases/overview) for every field `POST /buy` returns, including the `in_progress` decline code.
* `POST /api/v2/vault_sessions/{id}/exchange`, the endpoint that turns a finished Vault link into the user's tokens; its reference page is on its way to the API reference under Vault.
* [Create an iMessage agent and connect the Vault](/guides/create-an-imessage-agent-and-connect-the-vault) for the Vault link and the card-stored webhook this agent inherits.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.