# Outgoing API (v1.1)

OpenAPI spec: https://api.outgoing.world/partner/v1/openapi.json

REST API for partner integration with the Outgoing Local Experience Platform —
homescreen shelves, activity search, booking, and user provisioning.

## For AI agents

If you're an AI agent, prefer the machine-readable spec over this rendered page —
it parses more reliably than scraped HTML:

- **OpenAPI spec (JSON):** <https://api.outgoing.world/partner/v1/openapi.json> — the preferred format.
- **Docs index (llms.txt):** <https://api.outgoing.world/partner/v1/llms.txt> — Markdown index of endpoints and resources.
- **Full docs (llms-full.txt):** <https://api.outgoing.world/partner/v1/llms-full.txt> — every endpoint and schema as plain Markdown.

All three are public and require no authentication.

## Quick start

```bash
curl https://api.outgoing.world/partner/v1/homescreen \
  -H "Authorization: Bearer og_api_<your-key>" \
  -H "X-External-User-Id: <your-user-id>"
```

Every request needs a bearer API key (next section). Multi-user **negotiated**
keys also send an `X-External-User-Id` identifying *which* of your users the call
is for; single-user keys (self-serve) and AAuth agents omit it — they already
identify one user (see "Identifying the user" below).

## Authentication

API keys carry the `partner` scope and are issued per integration. Keys are
prefixed `og_api_` (production) or `og_api_staging_` (staging). Send them as
a bearer token:

```
Authorization: Bearer og_api_<secret>
```

Verify a key any time with `GET /partner/v1/auth` — it returns `200 {"ok": true}`
for a valid key (or `401` with a `code` otherwise) and does **not** count against
your daily quota.

### Identifying the user

How the acting Outgoing user is determined depends on your credential:

- **Negotiated key** (multi-user): you assert which user per request via
  `X-External-User-Id: <your-stable-user-id>`. The header is **required**.
  The first time you reference a given id, call `POST /partner/v1/users` to
  provision the Outgoing user and bind the external id to them; subsequent
  calls with the same id resolve to the same user. By asserting that id you
  attest that you have verified the end user's identity and have permission
  to act on their behalf. `POST /partner/v1/users` and `POST /partner/v1/bookings`
  are available only to negotiated keys.
- **Self-serve key**: bound to the single Outgoing user who issued it. Do
  **not** send `X-External-User-Id` — it identifies one user already, and a
  stray header is rejected with `400`.
- **AAuth agent**: acts as the agent's own mapped Outgoing user. Do **not**
  send `X-External-User-Id` (same `400`).

### Agent auth (AAuth)

As an alternative to a bearer key, AI agents may authenticate with **AAuth**
([aauth.dev](https://www.aauth.dev)) — cryptographic identity via HTTP Message
Signatures (RFC 9421: the `Signature-Input`, `Signature`, and `Signature-Key`
headers), with proof-of-possession on every request. A request with **no**
credential receives `401 Unauthorized` with a `WWW-Authenticate` header pointing
at our AAuth Resource Metadata (RFC 9728) plus an
`AAuth-Requirement: requirement=agent-token` header telling agents to send a
signed request with their agent token.

A new agent gets a small free trial; once exhausted it receives `202` with an
`AAuth-Requirement: requirement=interaction; url="…"; code="…"` header: send
your user to `{url}?code={code}` in a browser (and optionally display `code`
out of band so they can confirm the page matches). The `Location` header is
your poll URL — poll it (`GET /aauth/interaction?state=…`) until it returns
`200` once the user finishes the upgrade, then retry the original request.
Agents are subject to a daily call quota (`429` once exceeded).

## Rate limits

Limits are configured per API key (per-second, per-minute, per-hour). A
`429 Too Many Requests` response indicates you've exceeded them — back off
and retry after the current window resets. Contact us if your workload
needs a different ceiling.

## Errors

All error responses share one JSON shape:

```json
{ "error": "human-readable description" }
```

Authentication failures (`401`) also include a machine-readable `code`:
`key_malformed` (the value isn't a valid `og_api_` token — check for stray
whitespace or truncation) or `key_invalid` (the key isn't accepted — unknown,
disabled, or lacking the `partner` scope).

Status codes follow standard REST conventions — `401` for invalid credentials,
`404` for unknown resources, `400` for validation failures, `429` for rate
limits or daily-quota exhaustion, `500` for our bugs. Note: a request with **no**
credential returns `401` with a `WWW-Authenticate` discovery challenge, while an
AAuth agent that exhausts its free trial returns `202` with an upgrade URL — see
Agent auth (AAuth) above.

## Changelog

### v1.2 — June 2026

- `X-External-User-Id` is now **rejected with `400`** when sent by a self-serve
  key or an AAuth agent — those credentials already identify a single user.
  Negotiated keys are unchanged (the header remains required). See
  "Identifying the user".
- `POST /partner/v1/users` and `POST /partner/v1/bookings` are restricted to
  negotiated keys.

### v1.1 — April 2026

**Breaking change:** ticket pricing is now session-based.

- The `types` array has been removed from `ticket_price`.
- `ticket_price.sessions[]` is always present. Each session has `date`
  (`YYYY-MM-DD` or `null`), `time` (`HH:MM` or `null`), and a `tickets`
  array with `name`, `price`, `fee`, `total`, and `available` fields.
- Single-date events have one session; multi-date events have one session
  per date/time.
- `min`, `max`, `currency`, and `label` on `ticket_price` are unchanged.

## Endpoints

### GET /partner/v1/aauth/interaction

Poll Interaction

Agent polls here until the user completes the upgrade.

Returns 200 once the principal is upgraded, 202 (still pending) otherwise.

**Parameters**

- `state` (query, string, required)

**Responses**

- `200`: Successful Response
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### GET /partner/v1/activities/{activity_id}

Get Activity

Get activity details and record a SEEN interaction for the user.

**Parameters**

- `activity_id` (path, string, required)
- `Accept-Language` (header, string (nullable), optional) — BCP-47 language tag (e.g. `en`, `es`, `fr`). Controls the language of activity display names and descriptions. Defaults to `en`.
- `x-external-user-id` (header, string (nullable), optional) — The partner's external user ID. Required for negotiated multi-user keys; omit for self-serve keys and AAuth agents (sending it returns 400).

**Responses**

- `200`: Successful Response → `PartnerActivityDetail`
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### GET /partner/v1/activities/{activity_id}/prices

Get Activity Prices

Scrape the latest ticket prices for an activity.

Triggers an on-demand scrape of the activity's booking page and returns
the refreshed ticket pricing. Typically takes 5-15 seconds depending on
the ticketing platform.

**Parameters**

- `activity_id` (path, string, required)

**Responses**

- `200`: Successful Response → `PricesResponse`
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### GET /partner/v1/auth

Verify API key

Verify the supplied API key (or AAuth credential) is valid.

Returns ``{"ok": true}`` when accepted. Invalid credentials are rejected by
the middleware with ``401`` + a coarse ``code`` before this runs. Exempt from
the daily quota (free to poll lightly); still subject to per-key window rate
limits, so it can't be hammered.

**Responses**

- `200`: Successful Response
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### POST /partner/v1/bookings

Create Booking

Submit a booking request for an activity.

Accepts booking details including the activity, ticket count,
pre-authorized amount, and a `webhook_url` where the result will be
delivered once the booking completes (or fails).

The synchronous response confirms acceptance. The final result is
delivered asynchronously as a **BookingWebhookPayload** POST to
your `webhook_url`. See the `BookingWebhookPayload` schema for the
full callback payload structure.

**Request body** (`application/json`): `CreateBookingRequest`

**Responses**

- `202`: Booking request accepted for asynchronous processing. Results will be delivered to the `webhook_url` as a `BookingWebhookPayload` (see Webhook Callback below). → `CreateBookingAcceptedResponse`
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `402`: Payment required — a `live_purchase` was requested but this API key is not authorized to charge the stored payment card. Contact us to enable stored-card bookings for your key, or use a `dry_run_*` mode to test.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### GET /partner/v1/h3

Lat Lng To H3

Convert a latitude/longitude pair into an H3 cell index.

Convenience endpoint so partners don't need an H3 library client-side.
The default resolution matches what the ``/homescreen`` endpoint expects.

**Parameters**

- `lat` (query, number, required) — Latitude in decimal degrees.
- `lng` (query, number, required) — Longitude in decimal degrees.
- `resolution` (query, integer, optional) — H3 resolution (0–15). Defaults to 7 (neighborhood-sized, ~1.7 km edge).

**Responses**

- `200`: Successful Response → `H3CellResponse`
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### GET /partner/v1/homescreen

Get Homescreen

Return cacheable homescreen activities for a location and persona.

The partner sends an H3 resolution-7 (neighborhood-sized) cell
and an optional list of ``usual_plans`` that influence relevance ranking
via suitability boosts (e.g. kid-friendly, pet-friendly).
The cell is used only to determine the search centre; the actual
search radius uses density-based logic (ENTIRE_AREA base × max shelf multiplier).
The response includes ``Cache-Control`` headers indicating how long the caller should cache the response.

**Parameters**

- `h3_cell` (query, string, required) — H3 cell index at resolution 7 (~1.7 km edge). See [H3 Docs](https://h3geo.org/docs/api/indexing#latlngtocell) for how to compute this from a lat/lng. E.g. `872a100deffffff` for Brooklyn, NY or `872830828ffffff` for San Francisco, CA.
- `limit` (query, integer (nullable), optional) — Maximum number of activities to return per shelf. Defaults to 250 (the server maximum) when omitted.
- `usual_plans` (query, array<string> (nullable), optional) — User's usual plans — controls relevance ranking via suitability boosts.  Valid values: - `fun-with-kids` - `pet-friendly` - `date-nights` - `meet-new-people` - `friends-hangout` - `staying-active` - `nature-time`
- `shelved` (query, boolean, optional) — When true, activities are grouped into thematic shelves (e.g. indoor, outdoor, date night, kids) based on weather, persona, and time conditions. When false (default), all activities are returned in a single flat shelf.
- `booking_filter` (query, BookingFilter, optional) — Filter activities by booking availability.  - `all` (default): return all activities. - `bookable`: only activities with `is_bookable: true` (bookable via the POST /bookings endpoint). - `bookable_or_free`: activities that are bookable OR free (no ticket price / walk-in).
- `Accept-Language` (header, string (nullable), optional) — BCP-47 language tag (e.g. `en`, `es`, `fr`). Controls the language of activity display names. Defaults to `en`.

**Responses**

- `200`: Successful Response → `ShelvesResponse`
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### GET /partner/v1/orders

Get Orders

List bookings, deliverables, and emails for the authenticated partner user.

**Parameters**

- `Accept-Language` (header, string (nullable), optional)
- `x-external-user-id` (header, string (nullable), optional) — The partner's external user ID. Required for negotiated multi-user keys; omit for self-serve keys and AAuth agents (sending it returns 400).

**Responses**

- `200`: Successful Response → `OrdersResponse`
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### GET /partner/v1/search

Search

Search for activities and places using natural language.

Powered by the same agentic search engine as the Outgoing app: it interprets
the query, searches the live web, and enriches each pick. Returns ranked
results with the agent's rationale (``why``); call
``GET /activities/{activity_id}`` for full booking detail.

**Personalization** uses the resolved Outgoing user's biography, resolved by
priority: the key/AAuth principal's bound user → the ``X-External-User-Id``
mapping → anonymous. **Location** is a free-text hint; omitting it searches
globally (no default city).

Searches typically take 30 seconds to 2 minutes, so use a client timeout of at
least 120 seconds; a shorter timeout aborts the request even though the server
may still return a valid result, and the attempt still counts against your daily
quota.

**Parameters**

- `prompt` (query, string, required) — Natural-language search query (e.g. 'jazz concerts this weekend in Brooklyn', 'family-friendly brunch near the Mission').
- `location` (query, string (nullable), optional) — Optional free-text location hint (e.g. 'Paris', 'Brooklyn', 'near the Eiffel Tower'). Omit to search globally — there is no default city, so an unscoped query is a global search rather than a fallback to a specific city.
- `x-external-user-id` (header, string (nullable), optional) — The partner's external user ID, used to personalize search for negotiated multi-user keys. Optional; ignored for self-serve keys and AAuth agents.

**Responses**

- `200`: Successful Response → `SearchResponse`
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### POST /partner/v1/slack/workspaces/bind

Bind Slack Workspace

Bind the Slack workspace referenced by ``bind_token`` to this API key.

**Request body** (`application/json`): `BindWorkspaceRequest`

**Responses**

- `200`: Successful Response → `BindWorkspaceResponse`
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

### POST /partner/v1/users

Provision User

Provision (or update) a partner user mapping.

Creates an Outgoing user with the provided personalization signals and maps
the partner's external user ID to it.  Idempotent: if the mapping already
exists the profile and onboarding info are updated.

If the email or phone matches an existing Outgoing user, the mapping is
created pointing to that user **without** modifying their profile or
onboarding preferences.  The response will have ``linked=True`` and
``updated=False`` to indicate this.

By calling this endpoint the partner **attests** they have verified the
user's identity and have permission to act on their behalf.

**Request body** (`application/json`): `ProvisionUserRequest`

**Responses**

- `201`: Successful Response → `ProvisionUserResponse`
- `202`: Accepted, but interaction required. Returned to an AAuth agent that has used up its free trial (or has no bound user yet). The response carries an `AAuth-Requirement: requirement=interaction; url="…"; code="…"` header — send the user to `{url}?code={code}` in a browser (and optionally display `code` out of band so they can confirm the page matches) — plus a `Location` header with the poll URL (`GET /aauth/interaction?state=…`); poll until it returns 200, then retry the original request. (Bearer-key callers never see this.)
- `401`: Unauthorized — invalid API key, or no credential at all. Invalid-key responses carry a machine-readable `code`: `key_malformed` (not a valid `og_api_` token — check for stray whitespace/truncation) or `key_invalid` (not accepted — unknown, disabled, or missing the `partner` scope). A credential-less request instead carries a `WWW-Authenticate` discovery challenge (RFC 9728) plus an `AAuth-Requirement: requirement=agent-token` header so AAuth agents know to send a signed request.
- `422`: Validation Error → `HTTPValidationError`
- `429`: Rate limit exceeded. Back off and retry after the limit window resets.

## Schemas

### HTTPValidationError

- `detail` (array<ValidationError>, optional)

### PartnerActivityDetail

- `activity_id` (string, required)
- `name` (string, required)
- `short_description` (string (nullable), optional)
- `picture_url` (string (nullable), optional)
- `location` (object (nullable), optional)
- `semantic_location` (string (nullable), optional)
- `highlights` (array<string>, optional)
- `display_label` (string, optional)
- `is_bookable` (boolean, optional) — Does not indicate whether the activity requires booking, it indicates that ticket purchase is available through the Create Booking endpoint of this API.
- `ticket_price` (PartnerTicketPrice (nullable), optional) — Ticket pricing with individual ticket types available for booking.
- `estimated_fulfillment` (string (nullable), optional) — Expected fulfillment speed when booking. 'instant' — tickets are typically delivered within minutes. 'delayed' — fulfillment may take up to a few hours. Null if the activity is not bookable.
- `next_datetime` (string (nullable), optional) — Next upcoming event date/time in ISO 8601 format, or null if not scheduled.
- `booking_domain` (string (nullable), optional) — (Debug only) Ticketing platform domain. Populated only when `debug=true` query param is set.
- `venue` (string (nullable), optional) — Venue name where the activity takes place.
- `tips` (array<string>, optional) — Curated tips about the activity.
- `upcoming_datetimes` (array<string>, optional) — Upcoming event dates/times in ISO 8601 format, sorted chronologically.
- `picture_urls` (array<string>, optional)
- `is_open_now` (boolean (nullable), optional)
- `search_category` (array<string>, optional)

### PricesResponse

- `activity_id` (string, required)
- `ticket_price` (PartnerTicketPrice (nullable), optional) — Fresh ticket pricing after scrape, or null if no prices found.
- `scrape_status` (string, required) — Result of the scrape: 'success', 'no_prices', or 'error'.

### CreateBookingRequest

- `external_user_id` (string, required) — Your system's unique identifier for the user making this booking. Must have been provisioned via `POST /users` before calling this endpoint.
- `activity_id` (string, required) — UUID of the activity to book, as returned by the shelves or activity detail endpoints. Only activities with `is_bookable: true` can be booked.
- `tickets` (array<BookingTicketItem>, required) — List of ticket types and quantities to book. Each item specifies a ticket type name and how many to purchase.
- `authorization_amount_cents` (integer, required) — Total pre-authorized amount in cents (USD) for all tickets combined — not per ticket. For example, 2 tickets at $15.00 each should be `3000`. This amount is captured once the booking is confirmed.
- `idempotency_key` (string, required) — Client-generated unique key to prevent duplicate bookings. Use a UUID or similarly unique string per booking attempt. Replaying the same key returns the original booking (with its current status) without creating a new one. To retry a booking that **failed**, generate a NEW idempotency_key — replaying the original key returns the failed booking, not a fresh attempt.
- `webhook_url` (string, required) — URL we will POST to once the booking completes (or fails). Must be an HTTPS endpoint in production. HTTP is allowed for local testing with dry-run modes. The payload is a `BookingWebhookPayload` JSON object. Your server should respond with a 2xx status within 10 seconds.
- `payment_mode` (PaymentMode, required) — How to fulfill this booking. Required — one of: `live_purchase` (charge the shared payment card and book for real; your API key must be authorized for stored-card bookings or the request is rejected with 402), `dry_run_success` (simulate a successful booking, no charge), or `dry_run_failure` (simulate a declined/failed booking, no charge). The dry-run modes are for integration testing.

### CreateBookingAcceptedResponse

Synchronous response returned when a booking request is accepted for processing.

- `booking_id` (string, required) — Unique identifier for this booking, assigned by Outgoing.
- `idempotency_key` (string, required) — Echo of the idempotency key from the request.
- `status` (string, optional) — For a new booking, `accepted` — it is now being processed asynchronously. For a replayed `idempotency_key`, the existing booking's current status instead (e.g. `pending`, `processing`, `failed`, `confirmed`), so a replay never masks a failed booking as `accepted`. A booking that has already `failed` cannot be retried under the same key — use a new `idempotency_key`.

### H3CellResponse

- `h3_cell` (string, required) — H3 cell index at the requested resolution.
- `resolution` (integer, required) — H3 resolution used.
- `lat` (number, required) — Input latitude.
- `lng` (number, required) — Input longitude.

### ShelvesResponse

- `shelves` (array<PartnerShelf>, required)
- `h3_cell` (string, required)
- `city` (string, required)

### OrdersResponse

- `bookings` (array<OrderBooking>, optional)
- `deliverables` (array<OrderDeliverable>, optional)
- `emails` (array<OrderEmail>, optional)

### SearchResponse

- `message` (string, required) — Natural-language summary of the search results.
- `results` (array<PartnerSearchResult>, optional) — Search results, ranked by relevance. Each carries an activity_id — fetch full booking detail via GET /activities/{activity_id}.

### BindWorkspaceRequest

- `bind_token` (string, required) — The token from the bot's welcome DM.

### BindWorkspaceResponse

- `ok` (boolean, required)
- `team_id` (string, required)
- `bound` (boolean, required) — True if this request wrote the FK (vs. already bound).

### ProvisionUserRequest

- `external_user_id` (string, required) — Your system's unique identifier for this user. Used to reference the user in all subsequent API calls. Must be stable — changing this value creates a new user mapping.
- `first_name` (string (nullable), optional) — User's first name. Used for ticket purchases that require separate name fields.
- `last_name` (string (nullable), optional) — User's last name. Used for ticket purchases that require separate name fields.
- `display_name` (string (nullable), optional) — A name used to identify this user, e.g. for display personalization, bookings, and shared plans.
- `email` (string (nullable), optional) — User's email address. Optional — used for account recovery and transactional notifications.
- `phone` (string (nullable), optional) — User's phone number in E.164 format.
- `usual_plans` (array<string>, optional) — Personalization signals describing what this user typically enjoys. Controls which persona shelves are shown and how activities are ranked. Invalid values return a 400 error.  Valid values: - `visit-the-city` - `fun-with-kids` - `friends-hangout` - `date-nights` - `work-meetups` - `meet-new-people` - `staying-active` - `pet-friendly` - `nature-time` - `accessible-wheelchair-outings`
- `dietary_preferences` (array<string>, optional) — Dietary restrictions or preferences used to filter and rank food-related activities. Invalid values return a 400 error.  Valid values: - `vegan` - `vegetarian` - `kosher` - `halal` - `gluten_free` - `pescatarian` - `alcohol_free`
- `city` (string (nullable), optional) — User's home city slug (e.g. `new-york`, `san-francisco`). Used as a default macro location context when no explicit location is provided in a query.
- `neighborhood_lat` (number (nullable), optional) — Latitude of the user's home neighborhood center. Pair with `neighborhood_lng` to define a default radius used for proximity ranking.
- `neighborhood_lng` (number (nullable), optional) — Longitude of the user's home neighborhood center. Pair with `neighborhood_lat` to define a default radius used for proximity ranking.

### ProvisionUserResponse

- `external_user_id` (string, required)
- `created` (boolean, required) — True if a brand-new Outgoing user was created for this request.
- `linked` (boolean, optional) — True if the partner mapping was linked to a pre-existing Outgoing user matched by canonicalized email or phone. When linked, the existing user's profile and onboarding preferences are preserved (see `updated`).
- `updated` (boolean, optional) — True if onboarding info (usual_plans, dietary_preferences, neighborhood) was written to the user. False when linking to an existing user — their profile is preserved.

### ValidationError

- `loc` (array<string | integer>, required)
- `msg` (string, required)
- `type` (string, required)

### PartnerTicketPrice

Ticket pricing exposed to partners, with session-based ticket breakdown.

- `min` (number, required) — Minimum ticket price (including fees).
- `max` (number, required) — Maximum ticket price (including fees).
- `currency` (string, required) — ISO 4217 currency code (e.g. 'USD', 'EUR').
- `label` (string, required) — Human-readable price label (e.g. '$20-$50').
- `sessions` (array<PartnerSession>, optional) — Ticket availability grouped by session. Single-date events have one session.

### BookingTicketItem

A single ticket type and quantity within a booking request.

- `ticket_type_name` (string, required) — Name of the ticket type to book, exactly as returned in `ticket_price.sessions[].tickets[].name` (e.g. 'General Admission', 'VIP').
- `num_tickets` (integer, optional) — Number of tickets to book for this ticket type.
- `session_date` (string (nullable), optional) — Session date in YYYY-MM-DD format matching `ticket_price.sessions[].date`. Required for multi-session events: the (session_date, session_time) pair must exactly match one session (null matches null). For single-session events, may be omitted — the only session will be used.
- `session_time` (string (nullable), optional) — Session start time in HH:MM 24h format matching `ticket_price.sessions[].time`. Paired with `session_date` to disambiguate multi-session events.

### PaymentMode

How a booking is fulfilled.

``live_purchase`` charges the shared agentic-checkout card and books for real
(the key must be authorized via ``can_use_stored_card``). The ``dry_run_*``
modes simulate the respective outcome without any charge, for integration
testing.

_(no documented properties)_

### PartnerShelf

- `slug` (string, required)
- `display_name` (string, required)
- `activities` (array<PartnerActivity>, required)

### OrderBooking

- `id` (string, required)
- `status` (string, required)
- `activity_name` (string (nullable), optional)
- `picture_url` (string (nullable), optional)
- `authorization_amount_cents` (integer (nullable), optional)
- `created_at` (string, required)

### OrderDeliverable

- `id` (string, required)
- `deliverable_type` (string, required)
- `filename` (string, required)
- `content_type` (string, required)
- `file_size_bytes` (integer (nullable), optional)
- `partner_booking_id` (string, required)

### OrderEmail

- `id` (string, required)
- `subject` (string (nullable), optional)
- `classification` (string (nullable), optional)
- `processing_status` (string, required)
- `partner_booking_id` (string, required)
- `received_at` (string, required)

### PartnerSearchResult

A single agentic-search result.

Discovery-shaped (not the transactional ``PartnerActivity``): it carries the
agent's rationale (``why``) and editorial fields, plus ``activity_id`` —
fetch full booking detail (price, tickets, sessions) via
``GET /activities/{activity_id}``.

- `activity_id` (string (nullable), optional) — Outgoing activity id. Pass to GET /activities/{activity_id} for full booking detail. Null if the pick could not be persisted.
- `name` (string, required) — Activity or place name.
- `why` (string (nullable), optional) — Why this result matches the query — the agent's rationale.
- `short_description` (string (nullable), optional) — One-line description.
- `highlights` (array<string>, optional) — Short highlight tags.
- `venue` (string (nullable), optional) — Venue name where the activity takes place.
- `rating` (number (nullable), optional) — Aggregate venue rating (e.g. Google), if known.
- `next_datetime` (string (nullable), optional) — Soonest upcoming start time (ISO 8601), if the result is dated.
- `entity_type` (string (nullable), optional) — 'event' or 'place'.
- `picture_url` (string (nullable), optional) — Thumbnail image URL, if available.
- `location` (object (nullable), optional) — Geographic location of the activity.

### PartnerSession

A date/time session with its available tickets, exposed to partners.

- `date` (string (nullable), optional) — Session date in YYYY-MM-DD format, or null if not date-specific.
- `time` (string (nullable), optional) — Session start time in HH:MM (24h) format, or null.
- `tickets` (array<PartnerSessionTicket>, optional) — Tickets available for this session.

### PartnerActivity

- `activity_id` (string, required)
- `name` (string, required)
- `short_description` (string (nullable), optional)
- `picture_url` (string (nullable), optional)
- `location` (object (nullable), optional)
- `semantic_location` (string (nullable), optional)
- `highlights` (array<string>, optional)
- `display_label` (string, optional)
- `is_bookable` (boolean, optional) — Does not indicate whether the activity requires booking, it indicates that ticket purchase is available through the Create Booking endpoint of this API.
- `ticket_price` (PartnerTicketPrice (nullable), optional) — Ticket pricing with individual ticket types available for booking.
- `estimated_fulfillment` (string (nullable), optional) — Expected fulfillment speed when booking. 'instant' — tickets are typically delivered within minutes. 'delayed' — fulfillment may take up to a few hours. Null if the activity is not bookable.
- `next_datetime` (string (nullable), optional) — Next upcoming event date/time in ISO 8601 format, or null if not scheduled.
- `booking_domain` (string (nullable), optional) — (Debug only) Ticketing platform domain. Populated only when `debug=true` query param is set.

### PartnerSessionTicket

A ticket within a session, exposed to partners.

- `name` (string, required) — Ticket name (e.g. 'General Admission', 'VIP').
- `price` (number, required) — Ticket price in the activity's currency.
- `fee` (number, optional) — Additional fee on top of the base price.
- `total` (number, required) — Total the buyer pays (price + fee).
- `available` (boolean, optional) — Whether this ticket can currently be purchased.
- `description` (string (nullable), optional) — Additional description for this ticket.
- `on_sale_status` (string (nullable), optional) — Sale status (e.g. 'AVAILABLE', 'SOLD_OUT').
- `is_free` (boolean, optional) — True if this is a free/RSVP ticket.
- `min_quantity` (integer, optional) — Minimum number of tickets per order.
- `max_quantity` (integer (nullable), optional) — Maximum number of tickets per order, or null if unlimited.
- `category` (string (nullable), optional) — Ticket category (e.g. 'admission', 'add_on').
