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

# Integrate the Commerce SDK into an iMessage agent

> Teach an iMessage agent to shop at real stores with Agentcard's Commerce SDK and pay with the user's card from the Vault.

This guide takes the iMessage agent from [Create an iMessage agent and connect the Vault](https://docs.agentcard.com/guides/create-an-imessage-agent-and-connect-the-vault.md) 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 store through [Agentcard's Commerce SDK](https://docs.agentcard.com/vault/integrations/ecommerce-apis/purchase-api.md), shows the cart, and places the order when the user says yes, paying with the card they saved in the Vault.

The first guide covered the plumbing: Linq, Redis, and your Agentcard credentials. This guide covers the code that makes the agent shop.

What you end up with: a phone number where a user texts "two packs of AA batteries from Amazon", gets a photo, a name, and a price back within a minute, says which one they want, taps an approval link, and reads "order placed" in the same thread, with nothing else to type.

## Before you start

* An iMessage agent that receives a text and replies in the same thread. The first guide builds one on Linq, Vercel, and Redis.
* Agentcard sandbox credentials, from Commerce SDK, Start implementing in the dashboard. [Quickstart](https://docs.agentcard.com/commerce-sdk/quickstart.md) shows where.
* Node 18 or later.

> [!NOTE]
>
> The sandbox stops before payment. A confirm there ends `declined`, with no approval link and no order. Checking the approval and the placed order needs live credentials, as [Go live](#go-live) describes.

## How it works

The user's phone number is their `externalUserId`. Agentcard connects them to your company the first time they ask, with one Vault link, and never needs a code or an account.

| Text from the user | What the agent does |
| - | - |
| "two packs of AA batteries from Amazon" from a new user | Texts a Vault link alone in its own message. |
| (saves a card with Face ID) | Agentcard tells the agent. The agent runs the user's first ask in their words and texts the products. |
| (a search runs) | Texts `on it, looking for aa batteries` first, then a photo, a name, and a price for each product. |
| "the second one" | Adds exactly that product and texts the cart. |
| "yes" | Texts the approval link alone in its own message. |
| (taps the link, approves with Face ID) | Agentcard places the order and tells the agent. The agent texts the result. |

## 1. Install the SDK

```bash theme={null}
npm install @agent-cards/commerce-sdk
```

Build one `AgentcardClient` and keep one buy tool for each phone number, as [Quickstart](https://docs.agentcard.com/commerce-sdk/quickstart.md#give-your-agent-the-buy-tool) shows. Set `ack: 'text'` on each tool, because iMessage shows no reactions to the agent:

```ts agentcard.ts theme={null}
import { AgentcardClient, buyTool, type BuyTool } from '@agent-cards/commerce-sdk';

export const agentcard = new AgentcardClient({
  clientId: process.env.AGENTCARD_CLIENT_ID ?? '',
  clientSecret: process.env.AGENTCARD_CLIENT_SECRET ?? '',
});

const tools = new Map<string, BuyTool>();

/** The buy tool for one phone number, kept between texts. */
export function buyFor(phone: string): BuyTool {
  let buy = tools.get(phone);
  if (!buy) {
    buy = buyTool({ agentcard, externalUserId: phone, ack: 'text' });
    tools.set(phone, buy);
  }
  return buy;
}
```

## 2. Send texts, photos, and links through Linq

Linq sends a message from your number with a list of parts. A `text` part holds words, and a `media` part holds the web address of an image, which Linq fetches and delivers as a photo.

```ts linq.ts theme={null}
type Part = { type: 'text'; value: string } | { type: 'media'; url: string };

async function send(to: string, parts: Part[]) {
  const res = await fetch('https://api.linqapp.com/api/partner/v3/chats', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.LINQ_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ from: process.env.LINQ_PHONE_NUMBER, to: [to], message: { parts } }),
  });
  if (!res.ok) throw new Error(`Linq answered ${res.status}`);
}

export const sendText = (to: string, value: string) => send(to, [{ type: 'text', value }]);
export const sendPhoto = (to: string, url: string) => send(to, [{ type: 'media', url }]);
```

iMessage shows a URL that stands alone in a message as a preview card, and it mangles a URL with words attached. So every link goes out as its own message, never inside a sentence. For a Vault or approval link, anything glued to the URL turns it into a broken link that opens the Vault's sign-in page.

## 3. Answer each text in its own turn

Open a turn for each text and hand the app the turn in order with `onItem`. The turn sends the instant reply, then the product cards, then your model's words about them, and it holds each step link until the message that asks for it went out. [Answering each shopper message in its own turn](https://docs.agentcard.com/commerce-sdk/guides/answering-each-message.md) explains each option.

```ts chat.ts theme={null}
import Anthropic from '@anthropic-ai/sdk';
import { buyFor } from './agentcard';
import { sendPhoto, sendText } from './linq';

const anthropic = new Anthropic();

export async function onText(phone: string, text: string) {
  const turn = buyFor(phone).turn(text, {
    onItem: async (item) => {
      if (item.kind === 'text') return sendText(phone, item.text);
      const { link } = item;
      if (link.kind === 'product' || link.kind === 'store') {
        if (link.imageUrl) await sendPhoto(phone, link.imageUrl);
        await sendText(phone, [link.title, link.price].filter(Boolean).join(' '));
        if (link.url) await sendText(phone, link.url); // a flight has no page, so it sends no link
        return;
      }
      await sendText(phone, link.url ?? ''); // Vault, approval, or store sign-in: the link alone
    },
  });

  const history = await loadHistory(phone); // your code: the user's earlier messages, kept in Redis
  const runner = anthropic.beta.messages.toolRunner({
    model: 'claude-opus-5-5',
    max_tokens: 16000,
    tools: [turn],
    messages: turn.messages(history),
    stream: true,
  });
  for await (const stream of runner) {
    const paragraphs = turn.messageStream(); // each paragraph goes out as its own text
    stream.on('text', (delta) => paragraphs.push(delta));
    await stream.done();
    await paragraphs.end();
  }
  await saveHistory(phone, runner.params.messages); // your code
}
```

`loadHistory` and `saveHistory` stand for your own code. Save the history after every turn, because the model reads from it that the user saved a card or got an order.

Use the user's phone number in the same form every time, such as `+14155550100`, so the same person always maps to the same tool.

## 4. Hear from Agentcard at a webhook endpoint

A Vercel function ends each request after a few seconds, so take Agentcard's events at a webhook endpoint instead of `listen`. Mount `agentcardWebhook` on the route where the first guide points Agentcard, and run the next step for the user when a message advances the purchase. [Taking the next step once the shopper finishes outside the chat](https://docs.agentcard.com/commerce-sdk/guides/taking-the-next-step.md) explains the turn.

```ts app/agentcard/webhooks/route.ts theme={null}
import { waitUntil } from '@vercel/functions';
import { agentcardWebhook } from '@agent-cards/commerce-sdk';
import { buyFor } from '@/agentcard';
import { sendText } from '@/linq';

const webhook = agentcardWebhook({
  secret: process.env.AGENTCARD_WEBHOOK_SECRET ?? '', // returned once, when you create the endpoint
  onMessage: async (phone, message) => {
    const step = buyFor(phone).nextStep(message);
    if (!step) {
      await sendText(phone, message.text);
      await saveAssistantMessage(phone, message.text); // your code: the model reads it from the history
      return;
    }
    // Agentcard waits 10 seconds for an answer, so run the model after the response goes out.
    waitUntil(
      runNextStep(phone, step).catch(async () => {
        step.release();
        await sendText(phone, message.text);
        await saveAssistantMessage(phone, message.text);
      }),
    );
  },
});

export const POST = webhook.fetch;
```

`runNextStep` is `onText` for a turn that `nextStep` opened: it runs the model on `step` and sends the answer in place of `message.text`. `saveAssistantMessage` appends an assistant message to the user's history.

A Vercel function keeps a user's tool only while its own instance lives, and the chat route and this route can run in different instances. The next step reads where the purchase stands from Agentcard, so the card link, the cart, and the ask for the yes still arrive. A yes the user already gave in the chat, and the first ask of a user who was not connected yet, stay in the instance that took them, so the next step asks for the yes again, or asks the user to carry on from what they last asked for. To take both with nothing typed, run the chat and `agentcard.listen` together in one long-running process.

Create the webhook endpoint, subscribed by name to the three events that carry a message for a user. A catch-all `*` alone does not turn `purchase.message` on. Exchange your client ID and secret for an organization token first, as step 6 of the [first guide](https://docs.agentcard.com/guides/create-an-imessage-agent-and-connect-the-vault.md#6-point-agentcard-at-the-deployment) shows.

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/webhook_endpoints \
  -H "Authorization: Bearer $ORG_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://my-imessage-agent.vercel.app/agentcard/webhooks",
    "enabled_events": ["purchase.message", "order.confirmed", "order.updated"]
  }'
```

The response carries the endpoint's signing secret once. Save it as `AGENTCARD_WEBHOOK_SECRET` on Vercel and deploy again. The handler answers `400` to a delivery whose signature does not match, so until the secret is set, no message arrives.

## 5. Text it

From the phone you activated the Linq sandbox with, text the Linq number:

```text theme={null}
two packs of AA batteries from amazon
```

A new user gets a Vault link first, alone in its own message. Open it, connect with the code `111111`, and save any of [Stripe's published test cards](https://docs.stripe.com/testing). The sandbox sends no card-saved message, so text the agent again, and it texts the products for your ask. Send "the second one", and the agent adds that product and texts the cart. Send "yes", and the confirm ends `declined`, because the sandbox stops before payment.

## Go live

Going live makes the approval link, the passkey tap, and the placed order real. The code does not change.

1. Switch to Live credentials and choose how to pay the fee, as [Quickstart](https://docs.agentcard.com/commerce-sdk/quickstart.md#go-live) describes.
2. Replace `AGENTCARD_CLIENT_ID` and `AGENTCARD_CLIENT_SECRET` on Vercel with the production values.
3. Create a live webhook endpoint with the live credentials, because an endpoint belongs to a mode, and replace `AGENTCARD_WEBHOOK_SECRET` with its secret.
4. Deploy, then buy one inexpensive item with your own card. Check that the approval link arrives alone, and that "order placed" arrives after the tap with nothing typed.

## If nothing comes back

* The Vault link opens a sign-in page instead of the card form. Something was glued onto the URL. Send every link as its own message.
* The agent asks "did you save your card?" after the user did. The model never read the card message. Check that `saveAssistantMessage` runs, and that `loadHistory` returns what `saveHistory` stored.
* The card is saved and nothing happens. Check the endpoint's deliveries under Settings, Developers, Webhooks in the dashboard. A `400` means `AGENTCARD_WEBHOOK_SECRET` does not match the endpoint. A `500` means `onMessage` threw, and Agentcard delivers the event again.
* Messages arrive in the wrong order. Hand the turn to your app with `onItem` and write the model's text through `turn.messageStream()`.
* A product has no photo. The store sent none, so `imageUrl` is null. Send the name and price alone.

## Where to go next

* [Showing products and carting the one the shopper picks](https://docs.agentcard.com/commerce-sdk/guides/showing-products.md) for the product cards and the pick.
* [Approving a purchase](https://docs.agentcard.com/commerce-sdk/guides/approving-a-purchase.md) for the approval, the refusals, and the order messages.
* [Commerce SDK reference](https://docs.agentcard.com/api-reference/commerce-sdk/overview.md) for every option used here.


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