> ## 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.

# Company-funded accounts (no end-user wallet)

> Sandbox walkthrough for org-owned accounts: create a cardholder and a 30-day buy token, fund the company balance with test money, and let the agent buy with the buy token up to the sandbox wall.

export const BuildWithAI = ({prompt, tools}) => {
  const show = t => !tools || tools.includes(t);
  const copyPrompt = () => {
    if (typeof navigator !== 'undefined' && navigator.clipboard) navigator.clipboard.writeText(prompt);
  };
  const copyWithFlip = e => {
    copyPrompt();
    const label = e.currentTarget.querySelector('[data-label]');
    if (!label || label.getAttribute('data-copied') === '1') return;
    const original = label.textContent;
    label.setAttribute('data-copied', '1');
    label.textContent = 'Copied';
    setTimeout(() => {
      label.textContent = original;
      label.removeAttribute('data-copied');
    }, 2500);
  };
  const openPrefilled = base => {
    window.open(base + encodeURIComponent(prompt), '_blank');
  };
  const copyThenOpen = url => {
    copyPrompt();
    window.open(url, '_blank');
  };
  const chip = {
    display: 'inline-flex',
    alignItems: 'center',
    gap: '7px',
    padding: '7px 13px',
    margin: 0,
    borderRadius: '10px',
    border: '1px solid rgba(128,128,128,0.35)',
    background: 'transparent',
    color: 'inherit',
    font: 'inherit',
    fontSize: '14px',
    lineHeight: 1.2,
    cursor: 'pointer'
  };
  const iconProps = {
    width: 15,
    height: 15,
    viewBox: '0 0 24 24',
    fill: 'none',
    stroke: 'currentColor',
    strokeWidth: 2,
    strokeLinecap: 'round',
    strokeLinejoin: 'round',
    style: {
      flex: 'none',
      opacity: 0.85
    },
    'aria-hidden': true
  };
  return <div style={{
    border: '1px solid rgba(128,128,128,0.25)',
    borderRadius: '14px',
    background: 'rgba(128,128,128,0.06)',
    padding: '16px 18px',
    margin: '20px 0'
  }}>
      <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    fontWeight: 600
  }}>
        <svg {...iconProps}>
          <path d="M12 2 2 7l10 5 10-5-10-5Z" />
          <path d="m2 12 10 5 10-5" />
          <path d="m2 17 10 5 10-5" />
        </svg>
        <span>Build this with AI</span>
      </div>
      <p style={{
    margin: '6px 0 14px',
    fontSize: '13.5px',
    opacity: 0.65
  }}>
        Open a pre-built prompt in your preferred AI tool to generate a complete working integration.
      </p>
      <div style={{
    display: 'flex',
    flexWrap: 'wrap',
    gap: '8px'
  }}>
        {show('claude') && <button type="button" style={chip} onClick={() => openPrefilled('https://claude.ai/new?q=')}>
            <svg {...iconProps}>
              <path d="M12 3l2.1 6.9L21 12l-6.9 2.1L12 21l-2.1-6.9L3 12l6.9-2.1L12 3Z" />
            </svg>
            <span data-label>Open in Claude</span>
          </button>}
        {show('lovable') && <button type="button" style={chip} onClick={() => openPrefilled('https://lovable.dev/?autosubmit=true#prompt=')}>
            <svg {...iconProps}>
              <path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7Z" />
            </svg>
            <span data-label>Open in Lovable</span>
          </button>}
        {show('chatgpt') && <button type="button" style={chip} onClick={() => copyThenOpen('https://chatgpt.com/')}>
            <svg {...iconProps}>
              <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z" />
            </svg>
            <span data-label>Open in ChatGPT</span>
          </button>}
        {show('cursor') && <button type="button" style={chip} onClick={copyWithFlip}>
            <svg {...iconProps}>
              <path d="m3 3 7.07 16.97 2.51-7.39 7.39-2.51L3 3Z" />
            </svg>
            <span data-label>Copy for Cursor</span>
          </button>}
        {show('copilot') && <button type="button" style={chip} onClick={copyWithFlip}>
            <svg {...iconProps}>
              <path d="m16 18 6-6-6-6" />
              <path d="m8 6-6 6 6 6" />
            </svg>
            <span data-label>Copy for Copilot</span>
          </button>}
        {show('claude-code') && <button type="button" style={chip} onClick={copyWithFlip}>
            <svg {...iconProps}>
              <path d="m4 17 6-6-6-6" />
              <path d="M12 19h8" />
            </svg>
            <span data-label>Copy for Claude Code</span>
          </button>}
      </div>
    </div>;
};

This quickstart is for products where your company pays. Your users never open an Agentcard wallet, never add a card, and never add balance. Instead you create a cardholder for each user, request a buy token that lets their agent act as that one user, and every purchase draws on your company balance. By the end, you have a cardholder, a 30-day buy token, test money in the company balance, and an agent that has run a real purchase conversation at Amazon right up to the sandbox wall. About fifteen minutes, all in sandbox, so no money moves.

If your users pay with their own cards or their own balance, pick a wallet shape in [Choose a quickstart](/get-started/quickstarts) instead; the [web app quickstart](/get-started/quickstart) is the default.

<BuildWithAI prompt={"Build the Agentcard integration for company-funded accounts where the company pays and users never open a wallet, following\nhttps://docs.agentcard.sh/get-started/quickstarts/company-funded and the pages it links.\nGet sandbox credentials at https://app.agentcard.sh (Settings → Developers → Credentials).\nImplement the five steps the quickstart walks: credentials, create a cardholder and a buy token, the company balance, the agent buying, webhooks.\nRun every step for real in sandbox as you build (sandbox test money for the company balance comes from the CLI's companies balance test-fund command).\nObey every \"Rules for your agent\" block on the pages you use.\nThe integration passes when the purchase confirm comes back declined with the sandbox-mode reply:\nthat decline is the finish line, and in production the same call places the order.\nAsk me only for values you cannot derive: my credentials and the cardholder details to use."} />

## Prerequisites

* An Agentcard organization with sandbox credentials. In the [dashboard](https://app.agentcard.sh), 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.

- The Agentcard CLI, signed in and pointed at your organization: `npm install -g agent-cards`, then `agent-cards login` and `agent-cards companies use ORG_ID`. In sandbox the company balance is funded from the CLI. [CLI](/tools/cli) has the details.

<Steps>
  <Step title="Get your sandbox credentials">
    Your sandbox `client_id` and `client_secret` are in the [dashboard](https://app.agentcard.sh) 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:

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://api.agentcard.sh/api/v2/oauth/token \
        -d grant_type=client_credentials \
        -d client_id=YOUR_CLIENT_ID \
        -d client_secret=YOUR_CLIENT_SECRET
      ```

      ```javascript Node theme={null}
      const res = await fetch("https://api.agentcard.sh/api/v2/oauth/token", {
        method: "POST",
        body: new URLSearchParams({
          grant_type: "client_credentials",
          client_id: "YOUR_CLIENT_ID",
          client_secret: "YOUR_CLIENT_SECRET",
        }),
      });
      const { access_token: orgToken } = await res.json();
      ```

      ```python Python theme={null}
      import requests

      res = requests.post(
          "https://api.agentcard.sh/api/v2/oauth/token",
          data={
              "grant_type": "client_credentials",
              "client_id": "YOUR_CLIENT_ID",
              "client_secret": "YOUR_CLIENT_SECRET",
          },
      )
      org_token = res.json()["access_token"]
      ```
    </CodeGroup>

    The token comes back with its lifetime:

    ```json Output theme={null}
    {
      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "token_type": "Bearer",
      "expires_in": 3600,
      "scope": "api"
    }
    ```

    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.

    <CodeGroup>
      ```bash cURL 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://app.example.com/agentcard/webhooks", "enabled_events": ["*"]}'
      ```

      ```javascript Node theme={null}
      const res = await fetch("https://api.agentcard.sh/api/v2/webhook_endpoints", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${ORG_TOKEN}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ url: "https://app.example.com/agentcard/webhooks", enabled_events: ["*"] }),
      });
      const endpoint = await res.json();
      ```

      ```python Python theme={null}
      import requests

      res = requests.post(
          "https://api.agentcard.sh/api/v2/webhook_endpoints",
          headers={"Authorization": f"Bearer {ORG_TOKEN}"},
          json={"url": "https://app.example.com/agentcard/webhooks", "enabled_events": ["*"]},
      )
      endpoint = res.json()
      ```
    </CodeGroup>

    `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:

    ```json Output theme={null}
    {
      "id": "cmf3s1w7k0005l8m9dh4e2gq8",
      "object": "webhook_endpoint",
      "url": "https://app.example.com/agentcard/webhooks",
      "enabled_events": ["*"],
      "status": "active",
      "secret": "whsec_4f1e9c...",
      "livemode": false,
      ...
    }
    ```

    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.
  </Step>

  <Step title="Create a cardholder and a buy token">
    A cardholder is the person your company pays for. Create one with the org token; `firstName`, `lastName`, and `dateOfBirth` are required, and so is at least one of `email` or `phoneNumber`, because every cardholder needs a reachable identifier to complete onboarding later. The credential decides whether the cardholder is sandbox or live.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://api.agentcard.sh/api/v1/cardholders \
        -H "Authorization: Bearer $ORG_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{"firstName": "Ada", "lastName": "Lovelace", "dateOfBirth": "1990-01-15", "email": "ada@example.com"}'
      ```

      ```javascript Node theme={null}
      const res = await fetch("https://api.agentcard.sh/api/v1/cardholders", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${ORG_TOKEN}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          firstName: "Ada",
          lastName: "Lovelace",
          dateOfBirth: "1990-01-15",
          email: "ada@example.com",
        }),
      });
      const cardholder = await res.json();
      ```

      ```python Python theme={null}
      import requests

      res = requests.post(
          "https://api.agentcard.sh/api/v1/cardholders",
          headers={"Authorization": f"Bearer {ORG_TOKEN}"},
          json={"firstName": "Ada", "lastName": "Lovelace", "dateOfBirth": "1990-01-15", "email": "ada@example.com"},
      )
      cardholder = res.json()
      ```
    </CodeGroup>

    ```json Output theme={null}
    {
      "id": "cmf3r2x8e0004l8m9abcd1234",
      "firstName": "Ada",
      "lastName": "Lovelace",
      "email": "ada@example.com",
      "dateOfBirth": "1990-01-15T00:00:00.000Z",
      "phoneNumber": null,
      "createdAt": "2026-08-20T17:36:40.000Z",
      "updatedAt": "2026-08-20T17:36:40.000Z"
    }
    ```

    Keep the `id`. Creating the same email twice in one organization returns a `409` that carries the existing `cardholderId`, so a retry can pick up where it left off instead of dead-ending. Your first webhook, `cardholder.created`, fires here.

    Now request a buy token for the cardholder:

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://api.agentcard.sh/api/v1/cardholders/CARDHOLDER_ID/buy_token \
        -H "Authorization: Bearer $ORG_TOKEN"
      ```

      ```javascript Node theme={null}
      const res = await fetch(
        `https://api.agentcard.sh/api/v1/cardholders/${cardholder.id}/buy_token`,
        { method: "POST", headers: { Authorization: `Bearer ${ORG_TOKEN}` } },
      );
      const { buy_token: USER_ACCESS_TOKEN } = await res.json();
      ```

      ```python Python theme={null}
      res = requests.post(
          f"https://api.agentcard.sh/api/v1/cardholders/{cardholder['id']}/buy_token",
          headers={"Authorization": f"Bearer {ORG_TOKEN}"},
      )
      USER_ACCESS_TOKEN = res.json()["buy_token"]
      ```
    </CodeGroup>

    ```json Output theme={null}
    {
      "agentcard_user_id": "cmf3r5k1n0006l8m9qw8e7rt2",
      "buy_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "expires_at": "2026-09-19T17:37:12.000Z"
    }
    ```

    The buy token lasts 30 days and works only as that one user: it's the bearer the agent buys with, in exactly the place a connection token goes for wallet shapes, so export it as `USER_ACCESS_TOKEN` now. Store it keyed by your user and request a fresh one any time; there's no refresh. `agentcard_user_id` is the Agentcard user the cardholder is linked to, which is who the token acts as. From your coding agent, `mint_buy_token` on the [organization MCP server](/tools/mcp) does the same thing.
  </Step>

  <Step title="Fund the company balance">
    There's no end-user wallet in this shape. The company balance is what stands behind every purchase: in production you fund it and allocate spending power to cardholders from it, and the one-time card each confirm issues draws on that allocation. In sandbox you fund it with test money from the CLI:

    ```bash theme={null}
    agent-cards companies balance test-fund --amount 50
    ```

    ```text Output theme={null}
    Test pool now $50.00.
    ```

    Read it back with `agent-cards companies balance get`; the sandbox pool is its own line, separate from the live balance:

    ```text Output theme={null}
    Company balance
      Available:  $0.00
      ...
    Settings
      Company flow enabled:  yes
      ...
      Sandbox test pool:     $50.00
    ```

    This walkthrough never draws on the pool, because sandbox stops every purchase before anything is reserved; funding it here is what makes the shape complete, and it's what sandbox card issuing spends from when you get to that. If the CLI answers that the company balance isn't enabled on this environment, write to us and we'll switch it on.

    In production the mechanics are the same with real money: you fund the balance, allocate to a cardholder, and read their remaining company-funded spending power as `headroomCents` on `GET /api/v1/cardholders/:id`. A confirm that outruns the allocation declines rather than drawing on any other source; allocate more and send the same confirm again. [How the money moves](/purchase/purchase-api#how-the-money-moves) on the Purchase API page walks that funding path.
  </Step>

  <Step title="Let the agent buy">
    Wherever this step says the user's token, it's the buy token you exported as `USER_ACCESS_TOKEN`. It behaves identically as the bearer: one user, one conversation, and the same hash-bound confirm. Purchases made with it draw on your company balance, not a personal wallet.

    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.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://api.agentcard.sh/buy \
        -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{"ask": "a phone charger from Amazon, ship it to 548 Market St, San Francisco, CA 94104"}'
      ```

      ```javascript Node theme={null}
      const res = await fetch("https://api.agentcard.sh/buy", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${USER_ACCESS_TOKEN}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ ask: "a phone charger from Amazon, ship it to 548 Market St, San Francisco, CA 94104" }),
      });
      const turn = await res.json();
      ```

      ```python Python theme={null}
      import requests

      res = requests.post(
          "https://api.agentcard.sh/buy",
          headers={"Authorization": f"Bearer {USER_ACCESS_TOKEN}"},
          json={"ask": "a phone charger from Amazon, ship it to 548 Market St, San Francisco, CA 94104"},
      )
      turn = res.json()
      ```
    </CodeGroup>

    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:

    ```json Output theme={null}
    {
      "conversation_id": "cmemw5k2p00b7",
      "status": "needs_input",
      "reply": "I found a few options at Amazon. The Anker 30W USB-C charger is $15.99 and can arrive tomorrow. Want that one, or should I list alternatives?",
      "cart": null,
      ...
    }
    ```

    `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`:

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://api.agentcard.sh/buy \
        -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{"conversation_id": "CONVERSATION_ID", "ask": "the first one is fine"}'
      ```

      ```javascript Node theme={null}
      const res = await fetch("https://api.agentcard.sh/buy", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${USER_ACCESS_TOKEN}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ conversation_id: turn.conversation_id, ask: "the first one is fine" }),
      });
      const next = await res.json();
      ```

      ```python Python theme={null}
      res = requests.post(
          "https://api.agentcard.sh/buy",
          headers={"Authorization": f"Bearer {USER_ACCESS_TOKEN}"},
          json={"conversation_id": turn["conversation_id"], "ask": "the first one is fine"},
      )
      turn = res.json()
      ```
    </CodeGroup>

    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:

    ```json Output theme={null}
    {
      "conversation_id": "cmemw5k2p00b7",
      "status": "needs_input",
      "reply": "Anker 30W USB-C charger, $15.99. With the $0.40 service fee the total is $16.39, shipping to 548 Market St. Want me to place it?",
      "cart": {
        "merchant": "retail",
        "merchant_name": "Amazon",
        "items": [
          { "name": "Anker 30W USB-C Charger", "qty": 1, "priceCents": 1599 }
        ],
        "serviceFeesCents": 40,
        "tipCents": 0,
        "totalCents": 1639,
        "hash": "c47a91e02b3d5f68"
      },
      "error_code": null
    }
    ```

    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.

    <CodeGroup>
      ```bash cURL theme={null}
      curl -X POST https://api.agentcard.sh/buy \
        -H "Authorization: Bearer $USER_ACCESS_TOKEN" \
        -H "Content-Type: application/json" \
        -d '{"conversation_id": "CONVERSATION_ID", "confirm": "HASH_FROM_THE_CART"}'
      ```

      ```javascript Node theme={null}
      const res = await fetch("https://api.agentcard.sh/buy", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${USER_ACCESS_TOKEN}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ conversation_id: turn.conversation_id, confirm: turn.cart.hash }),
      });
      const receipt = await res.json();
      ```

      ```python Python theme={null}
      res = requests.post(
          "https://api.agentcard.sh/buy",
          headers={"Authorization": f"Bearer {USER_ACCESS_TOKEN}"},
          json={"conversation_id": turn["conversation_id"], "confirm": turn["cart"]["hash"]},
      )
      receipt = res.json()
      ```
    </CodeGroup>

    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:

    ```json Output theme={null}
    {
      "conversation_id": "cmemw5k2p00b7",
      "status": "declined",
      "reply": "This connection is in sandbox mode, and sandbox test cards cannot pay real merchants, so a live Amazon order can't be placed. To place real orders, connect with a production-mode client.",
      "cart": null,
      ...
    }
    ```

    <Check>
      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.
    </Check>

    Branch on `status`, never on the `reply` text: the prose varies run to run, the status and the cart don't. The [Purchase API](/purchase/purchase-api) page has the full envelope, multi-merchant confirms, and the rules a production integration needs.

    <Note>
      The buy token is also the bearer for the MCP server. Connect a client exactly as a wallet shape would, with the buy token in place of the connection token, and the agent gets the same `buy` tool:

      ```bash theme={null}
      claude mcp add agentcard-user --transport http https://mcp.agentcard.sh/mcp \
        --header "Authorization: Bearer BUY_TOKEN"
      ```

      [Buy through MCP](/purchase/mcp-for-agents) has the wiring and the rules for relaying the conversation.
    </Note>
  </Step>

  <Step title="See what happened">
    Open the [dashboard](https://app.agentcard.sh) 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:

    ```json connection.created theme={null}
    {
      "id": "evt_6f2c9a1d4e8b3f07a2c5d9e1",
      "type": "connection.created",
      "created": 1755711184,
      "livemode": false,
      "data": {
        "user_id": "cmf3qk5ab0002l8m9x1w2ab3c",
        "channel": "email",
        "external_user_id": "user_8213",
        "client_id": "5f1e8d2c-9a3b-4c7d-8e6f-1a2b3c4d5e6f"
      }
    }
    ```

    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](/ship/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](/ship/webhooks) shows the scheme and lists every event.
  </Step>
</Steps>

## Next steps

You created a cardholder, gave their agent a buy token, funded the company balance, and the agent ran a real purchase conversation to the confirm. In production the same five steps are your integration; only the credentials and the money change.

<CardGroup cols={2}>
  <Card title="Purchase API" href="/purchase/purchase-api">
    How the money moves for org-owned accounts, the full envelope, and the rules a production caller needs.
  </Card>

  <Card title="MCP" href="/tools/mcp">
    The organization server from your coding agent: cardholders, `mint_buy_token`, the company balance tools, and `test_charge`.
  </Card>

  <Card title="Webhooks" href="/ship/webhooks">
    Verify signatures and handle every event on your server instead of reading them in the dashboard.
  </Card>

  <Card title="Go live" href="/ship/go-live">
    Subscribe, switch to production credentials, and run the loop once with real money.
  </Card>
</CardGroup>
