---
title: "Agent API"
summary: "How an AI client connects to a LatticeKit business over MCP: the endpoint, how a grant is consented to and revoked, what the three scopes permit, why consequential actions park for a human, and how to find the right capability."
---

# Agent API

LatticeKit speaks the Model Context Protocol. An AI client outside the platform can
connect to a single business and work in it — reading the business, proposing changes,
and verifying that the changes worked.

## The endpoint

One route, streamable HTTP, stateless: one JSON-RPC message in, one JSON response out.
It supports `initialize`, `ping`, notifications, `tools/list`, `tools/call`, and
`resources/list` / `resources/read` for documentation. JSON-RPC batching is not
supported.

Clients discover where to authenticate through standard protected-resource metadata, then
register dynamically and walk authorise → consent → token.

## Grants

Everything hangs off a **grant**, not off the credential. An operator sees a consent
screen naming the client and chooses what it may do. Revoking the grant stops the client
immediately, without rotating or reissuing anything.

Scopes live on the grant:

| Scope | Permits |
|---|---|
| `agent:read` | Reading the business — schedules, availability, catalog, customers, orders, communications history. |
| `agent:write` | Tier-one actions only: check in a booking, record attendance, submit a form response. |
| `agent:propose` | The right to *ask* for a consequential action. Calling one does not perform it. |

A grant can also be narrowed to one location, or marked sandbox-only — which forces
neutralised side effects on the server, so a client cannot opt out by omitting a header.

## Why writes are tiered

`agent:write` is not "may do anything". It unlocks a small, deliberately boring set: the
repetitive actions a door scanner, a register or a kiosk performs, where nobody was
supervising any individual one anyway.

Everything above that tier parks. Calling it creates a proposal describing what would
happen and what it would cost to be wrong, and a person decides. This boundary lives in
the platform rather than in a prompt, so it holds regardless of what a model was
persuaded to believe.

Every write requires an idempotency key. See [errors](errors.md) — the retry here is a
machine's, and machines retry without knowing whether the first attempt landed.

## Finding the right tool

`tools/list` advertises a small fixed core rather than the full surface, because agents
degrade under schema sprawl. Discovery beats enumeration:

- `lattice_search_capabilities` — keyword search across everything this grant can reach.
- `lattice_describe_capability` — the full typed schema for one, and what it costs to be
  wrong.
- `lattice_invoke` — call any capability the grant permits, by id.

Anything visible stays callable through `lattice_invoke` whether or not it is advertised.
The short list is a context optimisation; the boundary is the grant.

## What an agent cannot do

- Reach an action a human operator could not, or reach it under weaker rules. The same
  checks apply to every caller.
- Execute a consequential action without a human. It can only ask.
- Exceed its daily ceilings on calls and money — a loop that goes wrong is bounded by
  policy, not by whoever notices first.
- See or write outside its granted scope. See [tenancy](tenancy.md).
- Act unaudited. Every call is recorded against the grant.

## Creating a business from an agent

An agent can also take a person from a description of their business to a live demo of
it to an account that keeps that demo — and come out the other side holding a grant to
it. This runs on a second, pre-tenant MCP endpoint, `POST /agent/v1/signup`, with the
same protocol shape as the one above (`initialize`, `ping`, `tools/list`, `tools/call`,
and the documentation resources) and a fixed set of tools.

**Credentials are arguments here.** There is no authorization server for this endpoint
and nothing to discover: a conversation's `secret` and a signup's `agentToken` are each
returned once, when the thing is created, and passed back as arguments on every later
call about it (the signup token is also accepted as `Authorization: Bearer`, for clients
that can set one). A credential given as an argument that fails is a tool result with
`isError: true`, never an HTTP 401. Every tool declares `securitySchemes: [{type:
"noauth"}]`.

Two things stay the person's and cannot be done by the agent: confirming the email
(where they also accept the terms and allow the agent's access), and — only while the
platform collects a card at signup — putting a card on file on the payment processor's
page. The agent relays instructions and links; every result carries a `nextStep` written
to be relayed.

### What is open

1. **`lattice_signup_mode`** — no credential. Returns `{ mode, waitlistOpen,
   cardRequired, conciergeMode, demoMode }`. While the platform is invite-only (`mode:
   "invite_only"`) every account tool below answers `403` with problem type
   `signup-closed`; the concierge tools answer to `conciergeMode` instead (`off` refuses
   `lattice_conversation_open` with `concierge-closed`). `cardRequired: false` means a
   confirmed email is the last step before the business exists. `demoMode: "on"` means
   live demo sites are being built.
2. **`lattice_starters`** — no credential, zero cost. The ten example businesses (a
   racquet club, gym, salon, studio, restaurant, barbershop, spa, clinic, hotel, music
   school), each with a summary and the default setup a preview of it has. Show them to
   someone who wants to look at an example, or begin from one.

### Describing, previewing, and a live demo

3. **`lattice_conversation_open { starter?, clientName }`** — no credential. Opens a
   conversation with the concierge, from a starter or from scratch. Returns
   `{ conversationId, secret, state, nextStep }`; the `secret` is shown once. Nothing is
   created.
4. **`lattice_conversation_say { conversationId, secret, text }`** — relay what the owner
   says about their business, in their words (at most 2000 characters a message), and get
   the concierge's reply, which may be a question to relay back. The transcript is what the
   preview is drafted from. `ready: true` means the concierge has enough. Under
   `conciergeMode: "free"` the reply comes from these documents rather than a model.
   Conversations are capped: twelve model turns, two previews, seven days.
5. **`lattice_conversation_preview { conversationId, secret, email, businessName? }`** —
   draft the business behind the owner's email: offers, hours, resources, staff, pricing,
   as LatticeKit would set it up. Returns `{ source, degraded, blueprint, state, nextStep }`
   and keeps the blueprint on the conversation.
6. **`lattice_conversation_demo { conversationId, secret }`** — build a live, bookable
   demo site of the previewed business: a real tenant at its own address, with the drafted
   offers and hours, that the owner can open and book in right now. Takes about a minute.
   One demo per conversation; it winds down after a week unless the owner creates the
   business, which then keeps it as-is. Refusals: `demo-no-starter` (a described business
   is not tied to a starter yet — the message lists the keys; pick one with
   **`lattice_conversation_starter { conversationId, secret, starter }`** and call again),
   `demo-closed`, `demo-capacity`, `demo-starter-unavailable`, and `demo-build-failed`
   (retry). Building is rate-limited per address.
7. **`lattice_conversation_state { conversationId, secret }`** — the transcript, the
   blueprint, the demo (its address and whether it is live), turns remaining, and what to
   do next.

### The account

8. **`lattice_signup_start { email, businessName, slug, clientName, conversationId?,
   secret? }`** — no credential. With the conversation, the business is created from it:
   its demo becomes the business, nothing is rebuilt, and its blueprint seeds the setup
   assistant; the conversation must have a preview or a starter (`concierge-state`
   otherwise). Without one, the account starts empty. `slug` becomes the business's
   address and must be free; `clientName` is what the person will see when asked to allow
   your access. Sends the confirmation email and returns `{ signupId, agentToken,
   conversationId, status, cardRequired, expiresAt, nextStep }`. The `agentToken` (prefix
   `sgat_`) is shown once and lives as long as the signup (48 hours). Nothing has been
   created yet. Terms are **not** accepted here — the person accepts them, and confirms
   that `clientName` may access the business once it exists, on the confirmation page. A
   repeat start for the same email inside the re-send cooldown is refused; one whose email
   has already been confirmed elsewhere answers `409`.
9. **`lattice_signup_status { signupId, agentToken }`** — returns `{ status,
   emailVerified, termsAccepted, agentAccessAllowed, cardOnFile, cardRequired,
   conversationId, tenantId, grantClaimed, expiresAt, nextStep, paymentUrl? }`. When a
   card is required, once the email is confirmed and the terms accepted it carries
   `paymentUrl` — the link the person opens to put a card on file; when they return the
   business is created and they are signed in as its owner. When no card is required,
   confirming the email creates the business at once (the person lands signed in), and
   `status` becomes `completed` on the next read.
10. **`lattice_signup_payment_link { signupId, agentToken, fresh? }`** — the same link on
    its own, for clients that want it separately. Refused before the email is confirmed or
    the terms accepted, and once a card is on file. Pass `fresh: true` only if the person
    reports the page has expired.
11. **`lattice_signup_complete { signupId, agentToken, slug? }`** — create the business now.
    Only once the email is confirmed and the terms accepted, and only when no card is
    required or one is on file (`signup-payment-method-required` otherwise, with the payment
    link to relay). The confirmation page creates the business itself under the no-card
    policy, so a `409` here means it already exists: claim the grant. `slug` changes the
    address, for a `tenant-slug-taken` answer.
12. **`lattice_signup_claim_grant { signupId, agentToken }`** — only once `status` is
    `completed`, and only once. Mints a grant on the new business — scopes `agent:read` +
    `agent:propose`, 30 days, 500 calls a day — and returns `{ apiKey, grantId, mcpUrl,
    tenantId, scopes, expiresAt, dailyCallLimit, nextStep }`. The `apiKey` is shown once;
    use it as the bearer at `mcpUrl`, the tenant endpoint described at the top of this page.
    A second claim answers problem type `signup-grant-claimed`.

What the person does, and where: on the confirmation page they accept the Terms of
Service and Privacy Policy and tick "Let *clientName* access *businessName* once it is
created" — both are required, and the agent cannot supply either; when a card is
required, on the payment page they add one, which never passes through LatticeKit or the
agent. They may open the payment link in a different browser from the one that confirmed
the email. A conversation an agent opened gets no welcome mail of its own; the
confirmation email is the one message the person receives before the business exists.

The grant the agent receives is the owner's decision, made on that confirmation page.
It appears in the console under Agents like any other connection and can be revoked
there at any time; it cannot write or move money, only read and propose. Refusals:
a signup tool with neither a bearer nor an `agentToken` argument is `401` (with a bare
`Bearer` challenge); a token for a different `signupId` is `403`; an expired signup is
`410`. Everything an agent can act on — validation, a wrong or missing conversation
secret, the re-send cooldown, the wrong state, every concierge and demo refusal, an
already-claimed grant — comes back as a tool result with `isError: true`, its text ending
in the problem type and HTTP status it corresponds to.

## Documentation

This document set is served over the same connection as MCP resources, addressed
`latticekit://docs/…`, and published as `llms.txt` for clients that have not connected.
It is the same set either way.

## Configuring a business

The interesting thing to do here is not fifty ordered calls. It is one document — see
[tenant as code](tenant-as-code.md).
