> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spyglass.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Conventions

> Ids, ordering, pagination, rate limits, and the things that surprise people.

## Brand ids

`{id}` in a path accepts either form returned by `/brands/search`:

* the **platform id** — a numeric string like `1234567890`
* the **brand UUID** — `a1b2c3d4-...`

Search returns the platform id as `id`, so passing that straight through is the
normal path. An id that matches no brand returns `404 brand_not_found`.

## Nothing is paginated

There is no cursor, no `page`, and no `total`. Each endpoint returns up to its
cap in one response, and `count` is **the number of rows in that response** —
not how many exist.

If `count` equals the `limit` you asked for, assume there is more data you
didn't get, and raise the limit.

## Ordering

| Endpoint                                  | Order                                                 |
| ----------------------------------------- | ----------------------------------------------------- |
| `/brands/search` with `mode: "smart"`     | relevance                                             |
| `/brands/search` other modes              | `sortBy`, default most followers                      |
| `/brands/{id}/media` with `mode: "smart"` | semantic relevance, unless the request implies a sort |
| `/brands/{id}/insights`                   | highest total first                                   |
| `/brands/{id}/landing-pages`              | most frequent first                                   |

Because results are ranked, truncating with `limit` keeps the strongest rows —
you lose the tail, not an arbitrary subset.

## Modes take different fields

`/brands/search` validates a different shape per mode, and these rules can't be
expressed in JSON Schema, so a generated client won't catch them:

* `mode: "ids"` requires `brandIds`. `count` is ignored — you get one row per id.
* `mode: "brand_name"` requires `brandNames` and `type`.
* `mode: "smart"` requires `query` and `type`, and **rejects** `sortBy`, because
  results are ordered by relevance.

Violations return `400 invalid_request` with the offending field in `details`.

## Rate limits

Three limits apply at once, and the lowest wins:

* **60 requests/minute per IP address**
* **120 requests/minute per key**
* a per-endpoint limit: 60/min for `/brands/search`, 30/min for the other three

Exceeding any of them returns `429 rate_limited` with a `Retry-After` header.
Wait that many seconds rather than retrying immediately.

Note the per-IP limit is lower than the per-key limit and is checked first, so a
single server making concurrent calls will hit 60/min before it hits 120/min.

## Requests and responses

* **Server-to-server only.** There's no CORS, so browser calls will fail. Keep
  the key on your backend.
* **POST bodies are capped at 100 KB.** Larger returns `413 payload_too_large`.
* **Responses are never cached** — every response carries
  `Cache-Control: no-store`.
* **`HEAD` is not supported** on `/insights` and `/landing-pages`. It returns
  `405 method_not_allowed`, because it would run the query and bill for it
  without returning a body.

## Fields can be added

Response objects are open. New fields may appear without a version bump, so
parse defensively and ignore what you don't recognize. Fields will not be
removed or change meaning without a new API version.
