Developers
The distil API
Create a study, have a conversation, read what was said.
Five-minute quickstart
Be the first respondent. You’ll need a terminal, a browser and an email address. The conversation itself takes about 5–10 minutes.
1. Get a key
Sign in at /keys and create a key. Keep it on your server, never in browser code. Get a key →
export DISTIL_API_KEY="dk_live_replace_with_your_key"2. Create a study
Save the returned study id and share_url. The brief tells distil what to ask about.
curl -sS https://api.distil.cx/v1/studies \
-H "Authorization: Bearer $DISTIL_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: first-study-1' \
-d '{"brief":"Find out what made signing up difficult.","question":"What was signing up like?"}'3. Answer it yourself
Open the share_url from step 2 in your browser and answer until the conversation finishes; it takes about 5–10 minutes. Lost it? This reads the study again; replace study_REPLACE with your study id.
curl -sS https://api.distil.cx/v1/studies/study_REPLACE \
-H "Authorization: Bearer $DISTIL_API_KEY"4. List the conversations
Replace study_REPLACE with the study id. Copy a conv_… id from data.
curl -sS 'https://api.distil.cx/v1/studies/study_REPLACE/conversations?status=completed' \
-H "Authorization: Bearer $DISTIL_API_KEY"5. Read what you said
Replace conv_REPLACE with that conversation id. The messages contain the transcript.
curl -sS https://api.distil.cx/v1/conversations/conv_REPLACE \
-H "Authorization: Bearer $DISTIL_API_KEY"In a signup flow
One study serves every user. Create it once (step 2) and store its share_url. Send each user to share_url?pid=<their user id>&ctx_plan=pro, or embed the study with data-pid. The study page reads pid and ctx_<key> from the URL; they come back on the conversation as participant_id and context. The character and length rules are the embed attributes’ (see Embed); a ctx_ key is the data-context- key without the prefix.
There are no webhooks in v1. To learn a conversation finished, poll the list from your server with status=completed. It is newest-started first, and a conversation can finish after later ones, so skip ids you have already stored. Page with starting_after=<last id of the page> while has_more is true. In the browser, an embedded study fires distil:done on document. To fetch one user’s conversations, add participant_id=<user id>; it composes with the other parameters.
curl -sS "https://api.distil.cx/v1/studies/$STUDY_ID/conversations?status=completed&participant_id=$USER_ID&starting_after=$LAST_SEEN" \
-H "Authorization: Bearer $DISTIL_API_KEY"
# Omit participant_id to list everyone. First page: omit starting_after. data[].participant_id is your user id; skip ids you stored.
# While has_more, set LAST_SEEN to the last data[].id and repeat.Embed in your product
The create response includes a ready-to-paste embed snippet; it draws the corner chat. Add data-pid with your user id (opaque); it comes back as participant_id. Attributes such as data-context-plan="trial" come back in context as {"plan":"trial"}. These are untrusted correlation values, not proof of identity.
API reference
Base URL: https://api.distil.cx/v1. HTTPS only: plain HTTP is redirected, so a key sent over it has already crossed in clear. Never use http://. Requests and responses are JSON. Authenticate with Authorization: Bearer dk_live_…. Keys belong to an owner; that owner’s keys share access to its API-created studies.
The default free budget is 100 respondent conversations started per owner per UTC calendar month, across API-created studies. It resets on the first day of the month. At the limit, new starts stop; conversations already started can finish. The owner allows 120 requests per minute, shared by all its keys; 429 responses carry a Retry-After header in seconds.
POST /studies
201 · Create a study. JSON: brief (required, 1–4000 characters), question (optional, ≤2000 characters; defaults to the brief’s first line), metadata (optional, ≤20 string entries; keys ≤40 characters, values ≤500). Optional Idempotency-Key: the same key and body return the original response for 24 hours; a different body returns 409 idempotency_conflict. A replay carries the response header Idempotent-Replayed: true; a first create does not.
GET /studies/{id}
200 · Read a study belonging to your owner.
GET /studies/{id}/conversations
200 · List respondent conversations, newest first; creator/setup conversations are excluded. Query: status (active or completed), limit (1–100, default 20), starting_after (a conv_… cursor), participant_id (only conversations started with that pid; 1–64 of A–Z a–z 0–9 _ -). Returns {object: 'list', data: [conversation without messages], has_more: boolean}. Pass the last id as starting_after for the next page.
GET /conversations/{id}
200 · Read a respondent conversation, including messages.
GET /openapi.json
200 · OpenAPI 3 document. No interactive Swagger UI. Key-management routes are excluded.
GET /keys
200 · Session cookie required, not Bearer auth. Returns {object: 'list', data: [{id, name, prefix, created, last_used}], usage: {month: 'YYYY-MM', conversations, limit}}. prefix is the key’s first 12 characters. usage counts respondent conversations started this UTC month against the free limit.
POST /keys
201 · Session cookie and a matching Origin required. JSON: {name?}. Returns {id, name, key}; the full key is shown only here.
DELETE /keys/{id}
200 · Session cookie and a matching Origin required. Returns {id, deleted: true}. Revoking a key does not remove its owner’s studies.
Study object
id (study_…), object (study), status (open or closed), brief, question, share_url (https://distil.cx/<share_token>), share_token, embed (the embed.js script snippet, with the corner chat launcher), metadata (string map), created (ISO 8601 UTC). The creator token is never returned.
Conversation object
id (conv_…), object (conversation), study (study_…), status (active or completed), participant_id (the pid from data-pid or ?pid=, or null), context (data-context-* or ?ctx_* string map), started, completed (null until completed). Dates are ISO 8601 UTC.
The single-conversation read adds messages: an ordered array of {role, content, created}. Role is assistant or user; content is text and created is ISO 8601 UTC.
Errors
{"error":{"type":"invalid_request_error","code":"invalid_param","message":"…","param":"brief"}}Every /v1 error uses this envelope. param names an invalid field, otherwise it is null. Another owner’s study or conversation returns 404, never 403.
- Send Authorization: Bearer dk_live_…
- The key is invalid or revoked.
- Sign in to manage keys.
- Correct the field named by param.
- The resource does not exist or belongs to another owner.
- Use a new idempotency key for a different body.
- The owner exceeded 120 requests per minute (shared by all its keys), or minted more than 10 keys in an hour. Wait the seconds in the Retry-After header.
- This path does not accept that method. The Allow header lists the ones it does.
- The body exceeds 1 MB.
- The request failed on the server.