---
title: "Test your AI agent"
description: "Point an AI agent at a sandbox through the Bookatu MCP server, with a test key or OAuth, and check what it did: the bookings it made, the messages it would have sent and the payments it would have taken, with nothing reaching a real client."
url: "https://bookatu.com/en-nz/developers/testing-ai-agents"
language: "en"
section: "Developers"
---

# Test your AI agent

Before an AI agent books, moves or cancels anything at a real business, run it against a sandbox. A sandbox answers the MCP server with the same tools and the same booking rules as a live business, and four tools of its own for checking what the agent did. Nothing it does reaches a real person or moves real money.

## Why test against a sandbox

- Mistakes stay in the sandbox. A wrong time, a double booking or a cancellation the agent should not have made touches made-up clients only.
- Nothing leaves Bookatu. Every confirmation, reminder and notice the agent causes is kept in the sandbox's test outbox, where the agent, or you, reads it with `sandbox_outbox`.
- Payments are simulated. A deposit is paid, declined or abandoned with `sandbox_simulate`, never with a card, and `sandbox_payments` lists every test payment with what became of it.
- Every answer says it is a test: the `Bookatu-Livemode` header is `false` on every answer, and `result._meta["com.bookatu/livemode"]` is `false` on every tool result, a refusal included. A successful result's `structuredContent` says `livemode: false` too.
- Every run can start from the same place. `sandbox_reset` puts the sample data back.

Make a sandbox first, and keep its first test key: see [Testing with a sandbox](https://bookatu.com/developers/testing).

## Connect an MCP client

The business MCP server has one address for every business, `https://bookatu.com/api/mcp` (Streamable HTTP, JSON-RPC 2.0, POST only). The business is the one the credential belongs to, so a sandbox's credential reaches that sandbox and nothing else. There are two ways in.

With the sandbox's test key: send it as `Authorization: Bearer bk_test_...` on every request (or as `X-Api-Key`). Send `Bookatu-Livemode: false` too where your client can: a call that would reach a live business, with a live key pasted in by mistake, is then refused before anything runs.

```http
POST https://bookatu.com/api/mcp
Authorization: Bearer bk_test_YOUR_KEY
Bookatu-Livemode: false
Content-Type: application/json
MCP-Protocol-Version: 2025-06-18

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}
```

With OAuth: a client that supports it signs in instead. It registers itself (dynamic client registration), sends you to Bookatu's consent page and gets a token of its own. Pick the sandbox there: your sandboxes are listed beside your businesses, marked (test). The token answer says `livemode: false`, and so does every MCP answer the connection gets. Tick the box that shares clients' names, emails and phone numbers when the agent should read the outbox (`sandbox_outbox` needs `customers:read`). A connected app can call every tool but `sandbox_reset`, which takes the sandbox's own test key.

A credential or a call of the other mode is refused as a JSON-RPC error, with `LIVEMODE_MISMATCH` in `error.data.code`, never as a result:

```http
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32000,
    "message": "This call asked for test mode (Bookatu-Livemode: false), and this is a live business. Test calls reach sandbox businesses only.",
    "data": { "code": "LIVEMODE_MISMATCH", "keyLivemode": false, "businessLivemode": true }
  }
}
```

## Claude Desktop

Claude Desktop, and Claude in a browser, adds a remote MCP server as a custom connector. In Claude, open Customize > Connectors and choose Add custom connector:

```
Customize > Connectors > Add custom connector

Name:                   Bookatu sandbox
Remote MCP server URL:  https://bookatu.com/api/mcp
Authentication:         Sign in now
OAuth client:           Register automatically
```

Claude then sends you to Bookatu: sign in, pick your sandbox (marked (test)), choose read and write, and approve. Where your organization has the Request headers section (a beta in Claude's dialog), you can use the test key instead: choose No sign-in, then add the header `authorization` with the value `Bearer bk_test_YOUR_KEY`, the word Bearer and its space included. A header name of your own needs Anthropic's approval first, so `Bookatu-Livemode` can't be added there; the test key on its own can only ever reach its sandbox.

Claude's own steps: [Add a connector that isn't in the directory](https://claude.com/docs/connectors/custom).

## Cursor

Cursor reads MCP servers from `.cursor/mcp.json` in a project, or `~/.cursor/mcp.json` for every project, and fills in `${env:NAME}` in a server's `url` and `headers`. Keep the key in your environment, never in the file:

```json
{
  "mcpServers": {
    "bookatu-sandbox": {
      "url": "https://bookatu.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${env:BOOKATU_TEST_KEY}",
        "Bookatu-Livemode": "false"
      }
    }
  }
}
```

Then set `BOOKATU_TEST_KEY` to your test key in the environment Cursor starts from. Leave `headers` out and Cursor signs in with OAuth instead (authorization code with PKCE, dynamic client registration), which opens Bookatu's consent page.

Cursor's own reference: [Model Context Protocol (MCP)](https://cursor.com/docs/context/mcp).

## MCP Inspector

The MCP Inspector is the reference tool for trying an MCP server by hand (it needs Node 22.19 or newer). Its CLI calls one method and prints the answer:

```sh
npx @modelcontextprotocol/inspector --cli https://bookatu.com/api/mcp --transport http \
  --header "Authorization: Bearer bk_test_YOUR_KEY" \
  --method tools/list

npx @modelcontextprotocol/inspector --cli https://bookatu.com/api/mcp --transport http \
  --header "Authorization: Bearer bk_test_YOUR_KEY" \
  --method tools/call --tool-name sandbox_outbox --tool-arg limit=5
```

Or open its web client on the server with `npx @modelcontextprotocol/inspector --server-url https://bookatu.com/api/mcp --transport http`, add the `Authorization` header in the server's settings (or sign in with OAuth), and call the tools from its Tools tab, where each tool's annotations and outputSchema show beside it.

The Inspector's own reference: [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector).

## Things to ask your agent

A short run that goes through a booking from start to end. Ask in plain words, and check each answer against what the tools returned:

1. Find a free time: "When could I book a haircut on Tuesday afternoon?" The agent calls `list_services` and `get_availability` (and `get_business` for the opening hours), and answers with times from the slots, on the business's clock.
2. Book it: "Book it for Mia, mia@example.com, +1 613 555 0142." `create_booking` answers the booking's `ref` and `appointmentId`. A sandbox's clients are made up: use an address at example.com and a number in a range kept for fiction.
3. Check the confirmation: "What was she sent?" `sandbox_outbox` shows the message that would have gone out (its `kind` is `booking_confirmation`), with its subject, its text and the booking's `appointmentId`.
4. Simulate the deposit paid: "The client has paid the deposit." `sandbox_simulate` with `deposit.paid` pays the booking's open test checkout: the deposit is paid, the answer lists the outbox messages and webhooks it caused, and `sandbox_payments` shows the checkout as paid. A booking needs an open test checkout for this, one opened on the sandbox's booking page or by a deposit the business asked for (`POST /api/v1/{slug}/appointments` with `requestDeposit: true`). `create_booking` books no service that takes a deposit (in the salon sample data, Full colour; a dental clinic's Whitening consultation; a restaurant's Tasting menu): it answers `DEPOSIT_NEEDS_BOOKING_PAGE` with `bookingPageUrl` and books nothing, as at a live business, so open that link to book it on the booking page and reach the test checkout there.
5. Cancel: "Cancel Mia's booking." `cancel_booking` cancels it as the business and says whether a deposit was kept. A cancellation by the business sends the client no message, so `sandbox_outbox` has no message to her about it: if she should hear, the agent or the business tells her. `sandbox_simulate` with `booking.client_cancelled` is the client's own cancellation instead, under the business's window, fees and notices; that one emails the client a cancellation notice, kept in the outbox, when the business has "Email clients when they cancel online" turned on in its settings.
6. Start again: "Reset the sandbox." `sandbox_reset`, with the sandbox's test key only, puts the sample data back. A good agent asks you first.

## What the sandbox tools return

| Tool | Scope | Returns |
| --- | --- | --- |
| `sandbox_outbox` | `customers:read` | `messages` (newest first: `id`, `channel`, `kind`, `to`, `subject`, `text`, `html`, `appointmentId`, `customerId`, `createdAt`, `meta`), `nextCursor` and `livemode: false`. |
| `sandbox_payments` | `reports:read` | `payments` (newest first: `id`, `kind`, `outcome`, `amountCents`, `refundedCents`, `currency`, `description`, `appointmentId`, `belongsTo`, `checkoutSessionId`, `paymentIntentId`, `createdAt`, `updatedAt`), `nextCursor` and `livemode: false`. |
| `sandbox_simulate` | `bookings:write` | `ok`, `event`, `appointment` (`id`, `status`, `depositStatus`, `depositCents`, `depositPaidCents`, `startAt`, `endAt`, `customerId`, `checkoutSessionId`), `outbox` (message ids), `webhooks` (event names), `result` and `livemode: false`. |
| `sandbox_reset` | `bookings:write` | `sandbox` (`id`, `slug`, `name`, `timezone`, `currency`, `createdAt`, `lastActiveAt`, `expiresAt`, `adminUrl`, `storefrontUrl`, `apiBaseUrl`) and `livemode: false`. |

They are in `tools/list` only at a sandbox, and only for a credential that may call them. Each has a title and every MCP annotation: `sandbox_outbox` and `sandbox_payments` are read-only and idempotent, each with an outputSchema its `structuredContent` fits; `sandbox_simulate` and `sandbox_reset` are destructive and not idempotent, and `sandbox_simulate` is open-world, since a sandbox's webhooks reach your endpoints.

A refusal the agent can act on is a tool error: for a client that sends a 2025 `MCP-Protocol-Version` header, a result with `isError: true` and `error.code` (`VALIDATION_FAILED` with `fieldErrors`, `APPOINTMENT_NOT_FOUND`, `NO_OPEN_CHECKOUT`, `SIMULATION_REFUSED`, or `UNKNOWN_SIMULATION` with the six `events`); for one that sends none, the JSON-RPC error `-32602` with the code in `error.data`. A missing scope is `403` naming `missingScope`, and a spent budget `429` with `retryAfterSec`.

Every other tool works at a sandbox as at a live business. A successful result says `livemode` in its `structuredContent` too, `false` here, and the text block says what it always said; a refusal (`isError: true`) has no `livemode` in its `structuredContent`, so read it there from `result._meta["com.bookatu/livemode"]` or the `Bookatu-Livemode` header.

## Limits

- A test key is held to 25 calls a minute and 1,000 a day, the sandbox's keys together. Every answer carries `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` and `RateLimit-Policy`, and a `429` carries `Retry-After`.
- `sandbox_outbox`: 30 calls a minute per sandbox, and 30 a minute per key, shared with GET /sandbox/outbox. The outbox keeps the newest 500 messages.
- `sandbox_payments`: 30 calls a minute per sandbox, and 30 a minute per key, shared with GET /sandbox/payments.
- `sandbox_simulate`: 10 calls a minute and 500 a day per sandbox, and 10 a minute per key, shared with POST /sandbox/simulate/{event}.
- `sandbox_reset`: 30 resets a day per address, shared with POST /api/developer/sandboxes/{id}/reset. Only the sandbox's own `bk_test_` key with `bookings:write`, never a connected app or a live key.
- The sandbox tools reach only the sandbox the credential belongs to: a booking of any other business is `APPOINTMENT_NOT_FOUND`. A live business never lists them, and called there, each is an unknown tool.
- Messages are kept, never sent, and cards never charged, so a real delivery can't be tested: read the outbox instead. Webhooks are the one thing a sandbox sends, to your own endpoints, with `livemode: false`.
- A sandbox has no Twilio, WhatsApp, Google Calendar or Stripe connection, and memberships can't be bought in test mode yet. The rest is in [Testing with a sandbox](https://bookatu.com/developers/testing).

When the agent works, see [Going live](https://bookatu.com/developers/going-live): the same tools at a live business, with its live key, and no sandbox tools.
