Skip to main content
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, @trycurious/node. Until the package is published to npm, call the API directly as shown on the Raw HTTP page and implement the behaviours it lists.
  2. Any other server language calls the API 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.
  • 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.
  • 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.
  • Keep the declaration in one place and send it on every secret-key call, or move it to a setup step with 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.
  • Use the same user_id for decide and events. When an anonymous user logs in, call 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.
  • 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.

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 with its experiment and the decision.
  4. Every response carries an X-Request-Id header. Log it; it is what support asks for.

When something is wrong

The troubleshooting pages are titled by symptom. The error envelope is always { "error": "<code>", "message": "<text>" }; the codes and the remedy for each are on Errors.