List clients (API key)
The client/guest directory, for a program. Shared by both engines and gated with no vertical for that reason: a client record means the same thing to a salon and a restaurant, which is why the dashboard screen behind it is one shared route too. There is consequently no wrong_vertical failure here.
Read-only, like every /api/v1 operation. As above, the handler's explicit company_id filter is the entire tenant boundary: removing it would leak every organization's client list.
Ordered newest first. No date filters: a client directory is not time-shaped, and there is no search parameter either, deliberately: a ?q= would be a third .or() call site to sanitize and pagination already lets a caller walk the whole list.
Authorization
bearerApiKey clients:readThe scheme for programmatic callers: partner integrations, a customer's own scripts, the planned
skill and MCP server. Accepted by the /api/v1/* operations and by nothing else; the rest of this
document is session-cookie only. Every :read scope below has a GET /api/v1/<resource> behind it, and
a static check fails the build if a scope is added without one. Full detail lives in docs/api-keys.md; the load-bearing
parts are:
- Transport.
Authorization: Bearer sk_live_…only. No query parameter, no custom header, no cookie fallback: a credential in a URL ends up in access logs, referrers and browser history. - A key is a program with scopes, not a human with a role. It carries no
admin/staffrole at all; scopes are the entire authorization model, and an admin issuing a key is deliberately delegating their own authority. - Scopes are
resource:action, where resource is exactly the/api/<resource>folder name and action isreadorwrite.writedoes NOT implyread: they are compared by exact string. - The engine boundary is enforced twice. Engine scopes are filtered at issuance against the org's
own
business_type(an appointments org cannot be grantedreservations:*), and again at use time by the gate's vertical argument.wrong_verticalis its own 403 code. - There is no scope for key management. A key can never mint, list or revoke another key, so a
leaked key cannot be used to establish persistence.
organizationis read-only for the same reason migration 0022 exists. - Errors carry a machine-readable
codealongsideerror:missing_authorization,invalid_key,key_revoked,key_expired,organization_inactive(all 401);insufficient_scope,wrong_vertical,plan_required(403);rate_limited(429). Match oncode, never on the sentence. Note this is a RICHER error body than the{error}every session-authenticated route in this document returns. - Rate limiting is per key, a fixed 60-second window defaulting to 120 requests/minute, with
Retry-AfterandX-RateLimit-*headers on a 429. Session-authenticated routes have no rate limiting in application code, deliberately: the rest of the app is covered at the edge by a Vercel WAF rate-limit rule, which is not billed for what it blocks and never reaches a function. Seedocs/api-keys.md. - One envelope, one paging scheme. Every list answers
{ data: [...], nextCursor }; the singleton answers{ data: {...} }.nextCursoris keyset (seek) paging, opaque, and null exactly when there are no more rows. Every field is camelCase, unlike the database columns underneath. /api/v1/is a real version segment, not decoration. There is nov2yet. When one ships,v1keeps answering unchanged for a notice window of at least 6 months, and everyv1response carriesDeprecationandSunsetheaders (RFC 9745, RFC 8594) for that whole window. Only a security fix can shorten it. Seedocs/api-keys.md.
In: header
Scope: clients:read
Query Parameters
Clamped, never rejected: values below 1 or unparseable fall back to 50, values above 200 become 200. A ?limit=1e9 from a program is far more likely to be a client bug than an attack, and answering 200 rows beats answering an error it retries in a loop.
1 <= value <= 20050The nextCursor from the previous response, passed back unchanged. Omit it for the first page. Opaque: it is base64url of an internal position and the encoding is not part of the contract; treat it as a token, do not construct one. A cursor this API did not issue is a 400 invalid_cursor rather than a silently-ignored parameter, because silently restarting at page one is how a paging loop becomes an infinite one.
The scheme is keyset (seek) paging on (order column, id), not OFFSET. OFFSET is wrong under concurrent writes: a booking created between page 1 and page 2 shifts every later row back by one, and the caller silently never sees whichever row slid across the boundary. Filters and limit may change between pages; the cursor only says where you got to.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/api/v1/clients"{ "data": [ { "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "name": "string", "email": "user@example.com", "phone": "string", "createdAt": "2019-08-24T14:15:22Z" } ], "nextCursor": "string"}Public booking MCP endpoint (JSON-RPC) POST
The Model Context Protocol endpoint an AI assistant connects to in order to read this platform's public booking surface on a guest's behalf. One endpoint for the whole platform, with the venue as a tool argument: both directories that can list an MCP server require it to call the operator's own first-party APIs from a domain matching the service, and no mainstream assistant discovers an MCP server from a web page at all, so a per-tenant endpoint would be unlistable by construction. **Not a REST operation, and the shape of this entry reflects that.** The body is JSON-RPC 2.0 and the method vocabulary is the MCP specification's (`initialize`, `tools/list`, `tools/call`, ...), not anything defined here; an OpenAPI schema for it would describe the transport and say nothing useful about the contract. Read the MCP specification for the envelope, and `tools/list` against this endpoint for the tools, which is the authoritative and self-describing answer. **Five tools, none of which books anything.** `get_venue`, `find_appointment_times` and `find_table_times` read; `book_appointment` and `book_table` turn a slot token into a short-lived link on the venue's own booking page, opened at that time, where the guest enters their own details and confirms. No tool accepts a guest's name, email or phone, none moves money, and none takes a table or provider out of inventory: opening a `book_table` link does cause the booking page to hold that table for about ten minutes, but the tool call itself holds nothing. Everything returned is already served to any anonymous visitor of `/book/{slug}`. **Findable on every plan, bookable from Growth.** `get_venue` answers any venue with its `bookingPage` link. The two time searches and the two booking tools need the venue's plan to include `ai_booking` (Growth and up, src/lib/billing/config.ts); below it they answer with `bookingPage` and `liveTimes: false` instead of times or a link at a time. **No timezone and no machine-readable instant is ever returned.** Times come back as display text plus an opaque HMAC-signed slot token, because a model handed an IANA zone next to a time converts it and books hours wrong. The token carries the real instant and is the only handle a future write tool will accept, which is also what stops a prompt injection moving a booking to another venue while the transcript still shows the right one. **No CORS, deliberately, and none is planned.** An MCP client is a server-to-server HTTP client. The browser-based MCP Inspector cannot reach this and is not meant to; use the Inspector CLI. Authentication: none, and that is the design rather than a gap. This surface holds no credential and reaches no tenant-private data. The separate PRIVATE per-tenant MCP server, which is not built, binds one `sk_live_` key to one company and never accepts a venue as a tool argument; the two trust models are deliberately not blended.
Read the organization (API key) GET
The calling organization's own profile and settings. **Shared**, and this is the operation that makes the rest of the surface navigable: `businessType` here is how a program learns which engine it is talking to, and therefore which of the two sets of endpoints below apply to it. Guarding it to one vertical would make that undiscoverable. A **singleton**, so the envelope is `{ data: { … } }` with an object and there is no `nextCursor`, no `limit` and no `cursor`. One envelope, always `data`, so a caller unwraps every v1 response the same way. `timezone` is the org's own IANA zone, and it is what the wall-clock times on `GET /api/v1/service-periods` are expressed in **and what `startsAt`/`endsAt` on the two booking lists are expressed in too**. A program reading bookings needs this endpoint to interpret them correctly, so a key granted only `appointments:read` or `reservations:read` cannot resolve them; grant `organization:read` alongside. See the timestamps section of `docs/api-keys.md`. `currency` and the `deposit*` fields are what a program needs to quote a deposit without guessing. There is deliberately **no `organization:write` scope** to pair with this. Migration 0022 exists because a tenant could edit its own `slug`, `status` and `business_type` outside the app; handing that back to a long-lived program-held credential would undo it. `solvintia_client_id` is withheld: it links this org to a record in a different Solvintia product and means nothing here.