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

# AI onboarding

> For AI coding agents integrating Curious: where the machine-readable documentation is, which surface to use, and the rules an integration must follow.

This page is addressed to an AI coding agent that has been asked to integrate Curious into a codebase. A human can read it too; it is the shortest complete statement of what a correct integration does.

## Read the documentation

* The index of every page is at `https://docs.trycurious.ai/llms.txt`. Fetch it first and open only the pages the task needs.
* Every page is available as Markdown by appending `.md` to its URL, for example `https://docs.trycurious.ai/guides/decide.md`. The whole site in one file is `https://docs.trycurious.ai/llms-full.txt`.
* The API contract is OpenAPI 3.1 at `https://docs.trycurious.ai/openapi/curious.openapi.yaml`. Field names, enums, limits and error messages in this documentation are quoted from it.

## Choose the surface

1. Detect the runtime. Server-side JavaScript or TypeScript (Node 22+, Deno, Bun, Cloudflare Workers, Supabase Edge Functions) uses the [server SDK](https://docs.trycurious.ai/sdks/node.md), `@trycurious/node`. Until the package is published to npm, call the API directly as shown on the [Raw HTTP](https://docs.trycurious.ai/sdks/http.md) page and implement the behaviours it lists.
2. Any other server language calls the [API](https://docs.trycurious.ai/api-reference/overview.md) directly: four `POST` endpoints, JSON, a bearer key.
3. Browser code holds only a **publishable key** (`curious_pk_`) and may call only `decide` for an element a secret key has already declared, and `events`. Never put a secret key (`curious_sk_`) in browser code, a mobile app, or a public repository.

## Rules an integration must follow

* **Read the key from the environment.** Use `CURIOUS_SECRET_KEY` for the secret key. Do not hard-code a key or commit one. Start with a test key (`curious_sk_test_…`); see [Keys and environments](https://docs.trycurious.ai/guides/keys.md).
* **Every `decide` has a deadline and a fallback.** Render your own content when the deadline passes or the call fails with any status. Do not retry a `decide` unless you send the same `idempotency_key`; a retry without one creates a second decision. See [Fallbacks and timeouts](https://docs.trycurious.ai/guides/fallbacks.md).
* **Call `decide` once per exposure**, when the content is about to be shown, not on every render. Each call records a decision, and a new decision for the same user and element ends the previous one's observation window. See [Assignment and stickiness](https://docs.trycurious.ai/concepts/assignment.md).
* **Keep the declaration in one place** and send it on every secret-key call, or move it to a setup step with [`declare`](https://docs.trycurious.ai/guides/declare). An identical declaration is a no-op. Changing `control`, `rewards`, `format`, `requirements`, `parameters` or `options.holdout` restarts the experiment, so these must not drift between call sites. See [Declarations](https://docs.trycurious.ai/concepts/declarations.md).
* **Use the same `user_id` for `decide` and `events`.** When an anonymous user logs in, call [`identify`](https://docs.trycurious.ai/guides/identify) from the server once, and await it before the next `decide` that should see the merged profile.
* **Give every event a stable `id`** so a retry is deduplicated, and retry only network failures, `429` after `Retry-After`, and `5xx`. Never retry a `400`, `401`, `403` or `409`, with one exception: a `409` whose code is `lock_timeout` is retried once with the same request. See [Idempotency](https://docs.trycurious.ai/api-reference/idempotency.md).
* **Name events in the past tense** (`purchased`, `signed_up`) and put event facts in `properties`, not in the name. Never start a name with `$`.
* **Send wire names in snake\_case** (`user_id`, `window_seconds`). The server SDK takes camelCase (`userId`, `windowSeconds`) and converts. Unknown fields on `decide`, `declare` and `identify` are a `400` that names the field.
* **Expect the control at first.** New experiments serve the control while variants are generated, and by default generated variants wait for a person to approve them in the dashboard (`options.approval` defaults to `manual`). This is not an integration error. See [Approval](https://docs.trycurious.ai/concepts/approval.md).

## Verify the integration

1. A `decide` call returns `200` with `content`, `arm_id`, `decision_id` and `experiment_status` (`initializing` on a new element). Under a test key the response is real and nothing touches live data.
2. An `events` call for the same `user_id` returns `{ "status": "success" }`; a repeat with the same `id` returns `duplicate: true`.
3. The element appears in the [dashboard](https://app.trycurious.ai) with its experiment and the decision.
4. Every response carries an `X-Request-Id` header. Log it; it is what [support](mailto:team@trycurious.ai) asks for.

## When something is wrong

The [troubleshooting pages](https://docs.trycurious.ai/troubleshooting/control-always-served.md) are titled by symptom. The error envelope is always `{ "error": "<code>", "message": "<text>" }`; the codes and the remedy for each are on [Errors](https://docs.trycurious.ai/api-reference/errors.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.