## Endpoint summary

| Verb | Path | Auth | Purpose |
|------|------|------|---------|
| POST | `/api_accounts` | provisioning | Create (idempotent by `uid`) |
| GET | `/api_accounts` | provisioning | List + usage + pagination |
| GET | `/api_accounts/:uid` | provisioning | Sync status and index size |
| DELETE | `/api_accounts/:uid` | provisioning | Destroy the account and its content |
| POST | `/api_accounts/:uid/resync` | provisioning | Re-crawl the website |
| PUT | `/api_accounts/:uid/connections/:connection_uid` | provisioning | Connect a source (idempotent by uid) |
| GET | `/api_accounts/:uid/connections` | provisioning | List one customer's connected sources |
| GET | `/api_accounts/:uid/connections/:connection_uid` | provisioning | One source's status |
| DELETE | `/api_accounts/:uid/connections/:connection_uid` | provisioning | Purge one source and its content |
| POST | `/api_accounts/:uid/connections/:connection_uid/reauthorise` | provisioning | Fresh consent URL for a dead grant |
| GET | `/context` | customer **or** provisioning | RAG context (org-wide with the provisioning token) |
| GET | `/question` | customer **or** provisioning | Grounded answer with citations |
| POST | `/context/save` | customer | Write a note to that customer's memory |
| GET | `/ping` | customer **or** provisioning | Token health check |
| * | `/custom_connections/*` | customer | Push markdown content |

## Errors

### Provisioning API (`/api_accounts`)

| Code | Meaning |
|---|---|
| 401 | Bad or missing provisioning token |
| 402 | Organisation is not on the white-label plan, its subscription has lapsed, or the automatic bundle add failed at the payment provider |
| 403 | The token holder is not an organisation admin |
| 404 | Unknown `uid`, a `uid` belonging to another organisation, or an id that is not an API account |
| 409 | `resync` while a sync is already running |
| 422 | Invalid `uid`, `label`, `site_url` (bad URL, explicit port, or embedded credentials) or `webhook_url` (bad URL, embedded credentials, or a private/loopback target), or a `resync` on an account with no website |
| 429 | Rate limited; honour `Retry-After` |

### Connections API (`/api_accounts/:uid/connections`)

Same auth, same 401/402/403/429 as the rest of the provisioning API. The body carries an `error_code` alongside the message:

| Code | `error_code` | Meaning |
|---|---|---|
| 404 | `unknown_connection_type` | The `connection_type` is not a type Context Link has. The message lists the accepted values. |
| 403 | `connection_type_not_activated` | Your organisation has not activated that type. Turn it on yourself at `/white_label/connection_activations`. |
| 409 | `connection_limit_reached` | The customer already holds the maximum of that type, or of connections overall. |
| 409 | `connection_type_immutable` | That uid already holds a different type. Changing type is a delete and recreate. |
| 409 | `partner_credentials_required` | That OAuth type is turned on without your complete app configuration. Add it, and whitelist our redirect URI on your app, at `/white_label/connection_activations`. Google Drive requires its OAuth pair, Picker API key and Cloud project number. |
| 409 | `picker_not_ready` | The Google connection has not completed OAuth yet, so Picker cannot open or accept a selection. |
| 422 | `invalid_setup_fields` | A required setup field, or a `return_url`, is missing or invalid. `errors` carries one message per field. |
| 422 | `connection_type_not_reauthorisable` | `reauthorise` on a type that has no authorisation to renew. |
| 422 | `picker_not_supported` | A Picker endpoint was called for a non-Google connection. |
| 422 | `invalid_file_selection` | Google rejected a file id, the grant cannot read it, its type is unsupported, or the selection is empty or over the limit. |
| 422 | `authorisation_expired` | The Google access grant expired while preparing Picker or validating its selection. Re-authorise, then choose files again. |
| 503 | `picker_not_configured` | The partner's stored Google Picker configuration is incomplete or invalid. |

### Query API (`/context`, `/question`, `/ping`)

| Code | Meaning |
|---|---|
| 400 | Missing `query` parameter, or `/context/save` without a `namespace` |
| 401 | Missing `Authorization` header, **or** the organisation's subscription has lapsed |
| 402 | `/question` on an organisation whose plan doesn't include it |
| 403 | `/context/save` attempted with the provisioning token (org-wide mode is read-only) |
| 404 | Unrecognised token |
| 429 | Rate limited, or the monthly question allowance is spent |
| 503 | `/question` reached the model and it failed. The default payload has `result: "llm_unavailable"`, `answer: null`, empty `citations`, and `format: "markdown"`. With `response_format=structured`, it uses the shared structured envelope with `markdown: null` and empty `results`. Retry. |

`/context` and `/question` both accept the opt-in `response_format=structured` query parameter and then return the same `format`, `query`, `markdown`, and `results` envelope. If the parameter is absent or has any other value, each endpoint's existing payload is unchanged.

Note the asymmetry: a lapsed subscription reads as `401` on the query endpoints and `402` on the provisioning and custom-connections APIs. Both mean "check billing".

### Custom Connections API

Documented in full on the [Custom Connections page](/docs/custom-connections).

## Limits

| Limit | Value |
|---|---|
| API accounts | 10 per bundle; bundles auto-added on overflow |
| Website crawl | 500 pages per API account |
| Connections per API account | 10, and the memory connection is exempt |
| Websites per API account | 1, counting toward the 10 |
| Custom connections per API account | 6, counting toward the 10 |
| Connected sources per type per API account | 5, counting toward the 10 |
| Posts per sync per connected source | 500 by default; lower where a sync is expensive: 30 for YouTube, 50 for Web Mentions |
| OAuth redirect link lifetime | 15 minutes |
| Questions | One pool shared across your whole fleet, sized at 5,000 questions per bundle per month |
| Custom-connection quotas | 1,000 posts / 100MB per connection; 100KB per post body |
| Write rate limit | 5 per 10s per token **and** per organisation |

**10 is the binding ceiling.** The per-type numbers are maxima within it, not slices that add up to it. A customer can hold 6 custom connections or 5 mailboxes, but not both, because the total still has to come in at 10. The memory connection is the one exception: it never counts.

Questions come out of **one pool shared across your whole fleet, sized at 5,000 questions per bundle per month**. One busy customer draws it down for every other customer you serve.

The per-organisation write limit is shared across your whole fleet, so a bulk backfill across many customers serialises at 5 writes per 10 seconds. Spread large migrations out.
