# Lobby Booking for AI Agents

A public, **unauthenticated** API lets external AI agents discover and book
[Lobby Link](https://ro.am) scheduling pages without a Roam account. The booker
is identified solely by the email in the request — no bearer token is required.

## Enable agents on a lobby

In the lobby's settings (**Advanced** tab), turn on **Enable AI Agents**. The
toggle is gated by a feature flag; when agents are disabled (or the lobby is
inactive), the public endpoints respond as if the lobby does not exist
(`404` / `lobby_not_found`).

SEO / agent discovery also respects account-level SEO opt-out: lobbies that
are not publicly listed will not appear in site-wide discovery surfaces.

## Discovery

Agents can find bookable pages without hard-coding handles:

| Surface | What it provides |
|---------|------------------|
| `https://ro.am/sitemap.xml` | SEO-enabled booking pages |
| `https://ro.am/llms.txt` | "Bookable People and Scheduling Links" section for LLM agents |
| `{pageUrl}.json` | Machine-readable booking-form schema, duration options, hosts, and `actions` pointing at the API endpoints below |
| `{pageUrl}.llm` | Prose representation of the same page for language models |

Example JSON discovery URL: `https://ro.am/{handle}/{slug}.json`.

Each booking page also advertises these representations via HTTP `Link:
rel="alternate"` headers, in-page `<link rel="alternate">` tags, schema.org
`ReserveAction` / `ScheduleAction` JSON-LD, and a small on-page note for agents
that read the rendered DOM.

Use the field ids from `{pageUrl}.json` when submitting custom-question
`responses` on book.

## Endpoints

Base URL: `https://api.ro.am` (no `/v0` or auth). Routes are mounted under
`/v1/public/lobby/…` on the public handler (separate from bearer-authenticated
API routes).

### Get availability

```http
GET /v1/public/lobby/availability?handle={handle}&slug={slug}&from={iso8601}&to={iso8601}&timeZone={iana}&duration={minutes}
```

| Parameter | Required | Description |
|-----------|----------|-------------|
| `handle` | yes* | Lobby handle |
| `slug` | yes* | Page slug under the handle |
| `from` / `to` | no | ISO-8601 window. Accepts a timestamp **with offset** or date-only `YYYY-MM-DD` interpreted in `timeZone`. Default is roughly the next month; the window is capped at ~2 months (a further-out `to` is **clamped**, not rejected) |
| `timeZone` | no | IANA zone (default `UTC`). Slots are always returned as absolute Unix times either way |
| `duration` | no | Meeting length in minutes; must be one of the lobby's offered durations |

\*Missing handle/slug yields `400` / `missing_parameter`; unknown or agent-disabled lobbies yield `404` / `lobby_not_found`.

**Response:**

```json
{
  "slots": [
    { "start": 1784552400, "end": 1784554200, "spotsRemaining": 1 }
  ]
}
```

- `start` / `end` are Unix epoch **seconds** as JSON numbers.
- Pass a chosen slot's `start`/`end` back to `book` unchanged. An altered span
  is rejected (`409` conflict); hosts are re-resolved server-side.

### Book a meeting

```http
POST /v1/public/lobby/book
Content-Type: application/json
```

```json
{
  "handle": "acme",
  "slug": "intro",
  "bookingSlot": { "start": 1784552400, "end": 1784554200 },
  "timeZone": "America/New_York",
  "booker": { "email": "ada@example.com", "name": "Ada" },
  "additionalInvitees": [{ "email": "grace@example.com", "name": "Grace" }],
  "responses": [{ "fieldId": "…", "value": { } }],
  "notes": "optional"
}
```

| Field | Required | Notes |
|-------|----------|-------|
| `handle`, `slug` | yes | Same as availability |
| `bookingSlot.start` / `.end` | yes | Unix **seconds** from availability; whole-minute spans only |
| `timeZone` | yes | IANA zone for the booking |
| `booker.email` | yes | Identifies the booker |
| `booker.name` | no | Display name |
| `additionalInvitees` | no | Extra guests |
| `responses` | no | Custom fields only — booker name/email/notes auto-fill reserved form fields; use field ids from `{pageUrl}.json` |
| `notes` | no | Free-text notes |

Unknown JSON members are rejected (`400` / `invalid_json`) so typos surface
clearly. Max body size is 64 KiB.

**Response** (excerpt): booking `id`, `start`/`end` as **RFC 3339** strings
localized to the requested `timeZone` (unlike the unix-seconds request fields),
`status`, optional `meetingLink`, hosts (**display names only — no host
emails**), invitees.

## Response codes

| Status | Meaning |
|--------|---------|
| `200` | Availability returned / booking created |
| `400` | Missing/invalid parameters, invalid JSON, or invalid slot/duration |
| `404` | Unknown handle/slug, inactive lobby, or agents not enabled |
| `405` | Wrong HTTP method |
| `409` | Slot no longer available (or does not match availability) |
| `500` | Backend / calendar failure |

Error bodies use the standard catalog shape, e.g.
`{"error":"…","code":"lobby_not_found"}` on v0-style public error rendering
where applicable — see [Error Codes](/docs/guides/error-codes).

## Behavior and caveats

- **Booking is committed immediately.** A calendar event is created and
  confirmation emails are sent to the host and booker before the request
  returns. There is no pending confirmation step.
- **Rate limiting** is by client IP (no API client to key on). Further abuse
  controls (email confirmation, per-lobby caps) may follow.
- **Network permissions:** agents calling this API from hosted runtimes need
  outbound HTTPS to `api.ro.am` and, for discovery, `ro.am`.

## Time formats (quick reference)

| Context | Format | Common mistakes |
|---------|--------|-----------------|
| Availability query `from` / `to` | ISO-8601 with **offset** (e.g. `2026-07-20T09:00:00-07:00`) **or** date-only `YYYY-MM-DD` interpreted in `timeZone` | Z-only timestamps are fine if you mean UTC; bare local times without offset are not |
| Slot `start` / `end` (availability response & book request) | Unix epoch **seconds** as a JSON **number** | Do not send milliseconds, strings, or RFC 3339 here |
| Booking response `start` / `end` | RFC 3339 **string** localized to the requested `timeZone` | Different from the request slot units — convert carefully |

Pass availability slots back to `book` byte-for-byte on `bookingSlot.start` /
`bookingSlot.end`. Rounding or unit conversion will fail slot re-validation
(`409`).

### Network permissions for hosted agents

Agents running in sandboxed environments (MCP hosts, automation runtimes, etc.)
need outbound HTTPS to:

- `https://api.ro.am` — availability + book
- `https://ro.am` — discovery (`.json` / `.llm` / `llms.txt` / sitemap)

Allowlisting only `api.ro.am` is not enough if the agent first fetches the
booking page metadata from `ro.am`.

## Related

- [Lobby Embeds](/docs/integrations/lobby-embed) — embed booking UI on a website
- Authenticated lobby APIs: [`lobby.list`](/docs/api/lobby-list),
  [`lobby.booking.list`](/docs/api/lobby-booking-list),
  [`lobby.booked` webhook](/docs/webhooks/lobby-booked)