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
.mdto its URL, for examplehttps://docs.trycurious.ai/guides/decide.md. The whole site in one file ishttps://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
- 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. - Any other server language calls the API directly: four
POSTendpoints, JSON, a bearer key. - Browser code holds only a publishable key (
curious_pk_) and may call onlydecidefor an element a secret key has already declared, andevents. 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_KEYfor 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
decidehas a deadline and a fallback. Render your own content when the deadline passes or the call fails with any status. Do not retry adecideunless you send the sameidempotency_key; a retry without one creates a second decision. See Fallbacks and timeouts. - Call
decideonce 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. Changingcontrol,rewards,format,requirements,parametersoroptions.holdoutrestarts the experiment, so these must not drift between call sites. See Declarations. - Use the same
user_idfordecideandevents. When an anonymous user logs in, callidentifyfrom the server once, and await it before the nextdecidethat should see the merged profile. - Give every event a stable
idso a retry is deduplicated, and retry only network failures,429afterRetry-After, and5xx. Never retry a400,401,403or409, with one exception: a409whose code islock_timeoutis retried once with the same request. See Idempotency. - Name events in the past tense (
purchased,signed_up) and put event facts inproperties, 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 ondecide,declareandidentifyare a400that 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.approvaldefaults tomanual). This is not an integration error. See Approval.
Verify the integration
- A
decidecall returns200withcontent,arm_id,decision_idandexperiment_status(initializingon a new element). Under a test key the response is real and nothing touches live data. - An
eventscall for the sameuser_idreturns{ "status": "success" }; a repeat with the sameidreturnsduplicate: true. - The element appears in the dashboard with its experiment and the decision.
- Every response carries an
X-Request-Idheader. 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.