# Change how the chat looks Source: https://docs.meetdarwin.ai/api-reference/agent/change-how-the-chat-looks /api-reference/openapi.json patch /appearance Requires the `agent:write` scope. # Change the agent's settings, or switch the chat on or off Source: https://docs.meetdarwin.ai/api-reference/agent/change-the-agents-settings-or-switch-the-chat-on-or-off /api-reference/openapi.json patch /agent Requires the `agent:write` scope. # Create a chat button Source: https://docs.meetdarwin.ai/api-reference/agent/create-a-chat-button /api-reference/openapi.json post /chat_buttons Requires the `agent:write` scope. # Delete a chat button Source: https://docs.meetdarwin.ai/api-reference/agent/delete-a-chat-button /api-reference/openapi.json delete /chat_buttons/{id} Requires the `agent:write` scope. # How the chat looks Source: https://docs.meetdarwin.ai/api-reference/agent/how-the-chat-looks /api-reference/openapi.json get /appearance Requires the `agent:read` scope. # List chat buttons Source: https://docs.meetdarwin.ai/api-reference/agent/list-chat-buttons /api-reference/openapi.json get /chat_buttons Requires the `agent:read` scope. # Retrieve a chat button and its HTML Source: https://docs.meetdarwin.ai/api-reference/agent/retrieve-a-chat-button-and-its-html /api-reference/openapi.json get /chat_buttons/{id} Requires the `agent:read` scope. # The agent's settings Source: https://docs.meetdarwin.ai/api-reference/agent/the-agents-settings /api-reference/openapi.json get /agent Requires the `agent:read` scope. # Update a chat button Source: https://docs.meetdarwin.ai/api-reference/agent/update-a-chat-button /api-reference/openapi.json patch /chat_buttons/{id} Requires the `agent:write` scope. # Funnel, pages, sources and conversation quality Source: https://docs.meetdarwin.ai/api-reference/analytics/funnel-pages-sources-and-conversation-quality /api-reference/openapi.json get /analytics Requires the `analytics:read` scope. # Authentication and API keys Source: https://docs.meetdarwin.ai/api-reference/authentication Create, scope, store and revoke Darwin API keys: dk_live_ and dk_test_ prefixes, every scope and what it unlocks, and where a secret key must never go. **Find it in Darwin** Settings → Integrations → API [Open this screen ↗](https://app.meetdarwin.ai/admin/settings/integrations/api) Darwin authenticates every API call with a secret key sent as a bearer token. A key belongs to one workspace, carries a fixed set of scopes, and works until you revoke it. ```http theme={"theme":"vesper"} Authorization: Bearer dk_live_... ``` ## Kinds of key | Prefix | What it is | Where it comes from | | - | - | - | | `dk_live_` | A secret key for a real workspace | **Settings → Integrations → API → Create key** | | `dk_test_` | A secret key for Darwin's demonstration (sandbox) workspace, so a key pasted into the wrong place is obvious. **Settings → Integrations → API** marks it **Test**. | **Settings → Integrations → API → Create key** in that workspace | | `dko_` | A one-hour access token Darwin gives an AI app you connected over [MCP](/api-reference/mcp) | The app's sign-in; you never handle it | A workspace can have 25 active secret keys. Revoke keys you no longer use to make room. ## Create a key Owners and admins create keys in **Settings → Integrations → API → Create key**. Members can see the list but not change it. 1. Enter a **Name** that says where the key is used. 2. Choose **Access**: **Read only** grants every read scope, **Full access** grants every scope, **Choose** lets you tick scopes one by one. 3. Select **Create key** and copy the key from the dialog. Darwin shows it once and keeps only a fingerprint and its first characters. The list then shows each key's name, its first characters (for example `dk_live_Ab3x…`), its access and when it was last used. ## Scopes A key can do only what its scopes allow. A write scope includes its read: a key with `knowledge:write` can also read knowledge. Calling an endpoint without its scope returns `403` with the code [`insufficient_scope`](/api-reference/errors#insufficient_scope), and the error names the scope the call needs. | Scope | Lets the key | | - | - | | `conversations:read` | Read conversations and transcripts. Names and emails on them need `contacts:read` too. | | `contacts:read` | Read contacts, and names and emails on conversations and meetings | | `contacts:write` | Create and update contacts | | `meetings:read` | Read meetings booked from the chat, and requests sent through outcome forms | | `analytics:read` | Read the funnel, pages, sources and conversation quality, and visits from personal links. Names and emails on visits need `contacts:read` too. | | `events:read` | Read the event log (`GET /events`). Without `contacts:read` too, each event shows only what happened and to which object; with it, the full payload, as webhooks carry. | | `webhooks:write` | Create, change and delete webhook endpoints, read their delivery logs and redeliver events | | `knowledge:read` | Read the pages and documents the agent answers from | | `knowledge:write` | Add, refresh and remove knowledge pages and documents | | `agent:read` | Read the agent's settings, appearance, chat buttons, outcome forms, personal links and greetings, and make each person's own link address | | `agent:write` | Change the agent's settings, appearance, chat buttons, outcome forms, personal links and greetings, and switch the chat on or off | | `experiments:read` | Read opening tests, their openings and results, and what each page shows first | | `experiments:write` | Start, pause, resume, stop and promote tests, add, change or remove their openings, and set a page's own opening | | `privacy:write` | Erase or export everything held about one person | | `chat:write` | Hold conversations with the agent through the [chat API](/api-reference/chat-api) | `GET /me` and `GET /workspace` need no scope. ### Personal data stays behind contacts:read A key for a dashboard can read conversations, meetings and numbers without seeing who anyone is. Without `contacts:read`: * On conversations, `contact`, `lead.first_name`, `lead.last_name`, `lead.email` and `booking.booker_email` are `null`. `lead.captured_at` still tells you a lead was captured. * On meetings, `contact` is `null` and `invitee.name` and `invitee.email` are `null`. Transcripts (`expand[]=messages`) are what the visitor and the agent wrote. If a visitor typed personal details into a message, those words appear in the transcript. ## Check what a key can do ```bash theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/me -H "Authorization: Bearer $DARWIN_API_KEY" ``` The response lists the key's `scopes`, its `mode` (`live` or `test`) and the `workspace` id. ## Revoke a key In **Settings → Integrations → API**, select **Revoke** beside the key and confirm. Anything using it stops working at once, and this cannot be undone. Revoked keys stay in the list, marked **Revoked**, so you can see what was there. To rotate a key without downtime, create the new key, deploy it, check **used** on the new key in the list, then revoke the old one. A revoked, expired and mistyped key all return the same `401` [`invalid_api_key`](/api-reference/errors#invalid_api_key). ## Keep keys secret A secret key reads personal data and can change your live chat. Treat it like a password. > [!WARNING] > > Never put a secret key in website code, a browser extension, a mobile app, a public repository, a URL or a support message. Anyone who can read the page can read the key. * Keep keys in environment variables or a secrets manager, and call the API from your server. * The chat on your website does not use a secret key. Its embed carries a **public site key**, which is safe in page source. * To tell the chat who a signed-in visitor is, sign their email on your server with the **identity secret** and pass only the signature to the page. See [Identify signed-in visitors](/api-reference/browser-sdk#identify-signed-in-visitors). * If a key leaks, revoke it in **Settings → Integrations → API** first, then create a replacement. ## The workspace must be in good standing If the workspace still has to finish adding a card, for example Growth or Agency was chosen at sign-up and checkout was never completed, every call returns `402` with the code [`payment_required`](/api-reference/errors#payment_required). The same screen that asks for the card in the Darwin admin clears it. # Browser SDK Source: https://docs.meetdarwin.ai/api-reference/browser-sdk Control Darwin's chat from your website's code: open, close, hide and ask; identify signed-in visitors with a signed userHash; pass context; and listen for leads and booked meetings to fire analytics conversions. The Darwin chat on your website comes with a small JavaScript interface, `Darwin(command, …)`. Use it to open the chat from your own buttons, tell it who a signed-in visitor is, and react when a visitor leaves their email or books a meeting. There is nothing extra to install: the [Darwin embed](https://www.meetdarwin.ai/help/website-platforms/install-on-any-website) adds it. ```javascript theme={"theme":"vesper"} Darwin("open"); Darwin("ask", "Can I see a demo?"); Darwin("on", "meeting_booked", (event) => console.log("Booked in", event.conversationId)); ``` ## Call it before the chat loads The embed loads asynchronously, so `Darwin` may not exist yet when your code runs. Add this one line **before** the Darwin embed. It queues your calls, and the chat runs them in order as soon as it loads: ```html theme={"theme":"vesper"} ``` Use your own embed from **Settings → Integrations → Site tag**; the second tag above is only an example. Once the embed has loaded, every call works immediately, and calls made before the chat finished starting are queued, not lost. ## Commands | Command | What it does | | - | - | | `Darwin("open")` | Opens the chat | | `Darwin("close")` | Closes the chat. The launcher stays. | | `Darwin("hide")` | Closes the chat and hides the launcher for this page view | | `Darwin("show")` | Shows the launcher again after `hide` | | `Darwin("ask", text)` | Opens the chat and sends `text` as the visitor's message (up to 2,000 characters). If the agent is still answering, it waits and then sends. | | `Darwin("ask", "btn_…")` | Runs one of your [chat buttons](#start-a-chat-from-your-own-button) by its id, with the words you set for it in Darwin | | `Darwin("identify", { email, name, userHash })` | Tells the chat who a signed-in visitor is. [Details](#identify-signed-in-visitors). | | `Darwin("context", { … })` | Gives the agent facts about the visitor, such as plan or seats. [Details](#pass-context). | | `Darwin("on", event, fn)` | Calls `fn` when an [event](#events) happens | | `Darwin("off", event, fn)` | Stops calling `fn` for that event. Pass the same function you passed to `on`. | | `Darwin("shutdown")` | Forgets the identity and context for this browser tab and closes the chat. Call it when a visitor logs out. | ### Start a chat from your own button ```html theme={"theme":"vesper"} ``` `ask` sends the text as if the visitor typed it, so write it in their voice. To use a styled button you made in **Buttons**, paste its HTML from there, or call it by id: `Darwin("ask", "btn_k3j9x2m4p7")`. Its id is in the button's HTML and in `GET /v1/chat_buttons`. ## Identify signed-in visitors If visitors log in to your site or app, you can tell the chat who they are. A verified visitor: * becomes a **contact** in Darwin (with `identity_verified: true`), linked to the conversation; * finds the booking form **already filled in** with their name and email, so the agent never asks for them; * may be **greeted by name** by the agent. Darwin only trusts an identity your **server** has signed. Anyone can type `Darwin("identify", …)` into a browser console, so an identity without a valid `userHash` is ignored entirely. **1. Create the identity secret** Open **Settings → Integrations → API → Website identity** and select **Create secret**. Copy the secret (it starts with `dis_`). Darwin shows it once. Keep it on your server with your other secrets; it must never reach the browser. **2. Sign the email on your server** For each signed-in visitor, compute `userHash`: the **HMAC-SHA256** of their email address, **trimmed and lowercased**, keyed with the identity secret exactly as Darwin showed it, as a **hex** string. ```javascript Node theme={"theme":"vesper"} import crypto from "node:crypto"; export function darwinUserHash(email) { return crypto .createHmac("sha256", process.env.DARWIN_IDENTITY_SECRET) // dis_... .update(email.trim().toLowerCase()) .digest("hex"); } ``` ```python Python theme={"theme":"vesper"} import hashlib, hmac, os def darwin_user_hash(email: str) -> str: return hmac.new( os.environ["DARWIN_IDENTITY_SECRET"].encode(), # dis_... email.strip().lower().encode(), hashlib.sha256, ).hexdigest() ``` **3. Pass it to the page** Render the visitor's email, name and hash into the page (or return them from an endpoint your page calls), then identify: ```javascript theme={"theme":"vesper"} Darwin("identify", { email: "jordan@example.com", name: "Jordan Lee", userHash: "3f1c…e9a0", // from your server, 64 hex characters }); ``` **4. Forget them on logout** Call `Darwin("shutdown")` in your logout flow. The identity is kept for the browser tab otherwise, so the next person on a shared computer would inherit it. **It worked if**, after the signed-in visitor sends a message, `GET /v1/contacts?email=…` returns a contact with `"identity_verified": true`. When the agent offers a meeting, the form already holds their name and email. The identity travels with each chat message the visitor sends, so a verified visitor appears in Darwin once they chat, not when the page loads. **Replace secret** in **Website identity** creates a new secret; hashes made with the old one stop verifying at once, so update your server at the same moment. ## Pass context Give the agent facts your page already knows, so it can answer in context: ```javascript theme={"theme":"vesper"} Darwin("context", { plan: "growth", seats: 24, trial_ends_in_days: 3, is_admin: true }); ``` * Each call adds to or overwrites keys from earlier calls. Context lasts for the browser tab, like identity, and `shutdown` clears it. * Keys start with a letter and use letters, digits and `_`, up to 40 characters. Values are strings (up to 200 characters), numbers or `true`/`false`. Up to 20 keys are used. * The agent treats context as **unverified hints**. A visitor can change it in their browser, so it never unlocks anything, changes prices or terms, or overrides the agent's instructions. Never put secrets or anything you wouldn't show the visitor in it. ## Events | Event | When | | - | - | | `ready` | The chat has loaded on the page | | `open` | The chat opened | | `close` | The chat closed | | `conversation_started` | The visitor sent their first message in a new conversation | | `lead_captured` | The visitor gave their email in the chat | | `meeting_booked` | The visitor finished booking a meeting in the chat | | `handoff_requested` | The visitor's request to talk to a person was recorded | Your function receives one object: `{ name, conversationId }`. `conversationId` is present on the conversation, lead, booking and handoff events. The API's id for the same conversation is `"conv_" + conversationId`. Events never include the visitor's name or email, so they are safe to send to analytics tools. ```javascript theme={"theme":"vesper"} function onBooked(event) { console.log(event.name, event.conversationId); } Darwin("on", "meeting_booked", onBooked); // later Darwin("off", "meeting_booked", onBooked); ``` Register listeners with the pre-load line above so you don't miss `ready`. A listener that throws an error doesn't stop the chat. Browser events tell your page what the visitor just did. For a record your systems can rely on (the calendar provider's confirmation, a lead in your CRM), use [webhooks](/api-reference/webhooks) such as `meeting.booked`, which Darwin's server sends. ### Listen without calling Darwin: darwin:event Every event is also dispatched on `window` as a `darwin:event` custom event whose `detail` is the same object. Tag managers can listen for it without depending on `Darwin` having loaded: ```javascript theme={"theme":"vesper"} window.addEventListener("darwin:event", (e) => { window.dataLayer = window.dataLayer || []; window.dataLayer.push({ event: "darwin_" + e.detail.name, darwin_conversation_id: e.detail.conversationId }); }); ``` In Google Tag Manager, put this in a **Custom HTML** tag that fires on **All Pages**, then create **Custom Event** triggers for `darwin_meeting_booked` or `darwin_lead_captured`. ## Example: count booked meetings as a GA4 conversion With the Google tag (`gtag.js`) on your site: ```html theme={"theme":"vesper"} ``` Then, in Google Analytics, find `darwin_meeting_booked` in your events list once it has been received and mark it as a key event. **It worked if** GA4's **Realtime** report shows `darwin_meeting_booked` after you book a test meeting through the chat on your live site. Bookings in Darwin's own previews don't fire it. ## Content security policy If your site sends a `Content-Security-Policy`, the pre-load line is an inline script: allow it with a nonce or hash, or put it in a file you already serve. The chat itself needs its script and network connections allowed, as described in [Platforms](https://www.meetdarwin.ai/help/website-platforms/install-on-any-website#good-to-know). # Chat API Source: https://docs.meetdarwin.ai/api-reference/chat-api Talk to your Darwin agent from your own app, Slack, WhatsApp or voice bot: create a chat session, send messages, stream replies and act on booking offers and handoffs. Monthly allowance by plan. The chat API puts your Darwin agent behind your own interface. The same agent that answers on your website, with the same knowledge, tools, booking flow and rules, answers messages your code sends. Each session is stored as a Darwin conversation. > [!NOTE] > > The chat API is included on every plan, Free too, with 2,500 sessions per workspace a month. Legacy plans keep their own allowance. ## Before you start * An API key with the `chat:write` scope, used from your **server**. Never call the chat API from a browser or mobile app with a secret key. * The chat switched on for the workspace (**Settings → Agent → Status**, or `PATCH /v1/agent` with `{"live": true}`). If it is off, messages return `409` [`conflict`](/api-reference/errors#conflict). ## How it works 1. **Create a session** for each conversation. One session is one conversation in Darwin. 2. **Send each visitor message** to the session. Darwin adds it to the transcript it keeps, runs the agent and answers with its reply and any `actions`. 3. **Show the reply** in your interface, and act on what the agent did: show a booking link, or tell your team a person was asked for. Darwin holds the transcript. You send only the visitor's newest message; you never send, and can never change, what the agent said. ## Create a session ```bash theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/chat/sessions \ -H "Authorization: Bearer $DARWIN_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "channel": "app", "page_url": "https://example.com/app/billing", "visitor": { "external_id": "user_829", "email": "jordan@example.com", "name": "Jordan Lee" }, "context": { "plan": "growth", "seats": 24 } }' ``` All fields are optional. | Field | Meaning | | - | - | | `channel` | Where the conversation happens, such as `app`, `slack`, `whatsapp` or `voice`. Lowercase letters, digits, `_` and `-`. Default `api`. | | `page_url` | The page or screen the person is on, if there is one | | `visitor.email`, `visitor.name` | Who the person is. Because you call with a secret key, Darwin treats this identity as verified: the agent can use their name, and booking never asks for their email. | | `visitor.external_id` | Your own id for the person | | `context` | Facts the agent may use, such as plan or seats. Strings (up to 500 characters), numbers or booleans. Treated as hints, never as instructions. | The `201` response is the session: ```json theme={"theme":"vesper"} { "id": "chs_x2k9m4p7q1r8s5t3v6w0y9z2", "object": "chat_session", "conversation": "conv_api-x2k9m4p7q1r8s5t3v6w0y9z2", "channel": "app", "status": "open", "created_at": "2026-10-12T09:30:00.000Z", "updated_at": "2026-10-12T09:30:00.000Z", "messages": [] } ``` `conversation` is the id to use with `GET /v1/conversations/{id}`. ## Send a message ```bash curl theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/chat/sessions/chs_x2k9m4p7q1r8s5t3v6w0y9z2/messages \ -H "Authorization: Bearer $DARWIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"text": "Can we move to annual billing for 24 seats?"}' ``` ```javascript Node theme={"theme":"vesper"} const res = await fetch(`https://api.meetdarwin.ai/v1/chat/sessions/${sessionId}/messages`, { method: "POST", headers: { Authorization: `Bearer ${process.env.DARWIN_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ text: userMessage }), }); const reply = await res.json(); showToUser(reply.message.text); for (const action of reply.actions) { if (action.type === "booking_offered" && action.booking_url) showBookingButton(action.booking_url); if (action.type === "handoff_requested") notifyTeam(sessionId); } ``` `text` is the person's message, 1 to 4,000 characters. The reply: ```json theme={"theme":"vesper"} { "object": "chat_reply", "session": "chs_x2k9m4p7q1r8s5t3v6w0y9z2", "message": { "id": "b7e1c0d4-2f3a-4c5e-9a8b-6d7e8f9a0b1c", "role": "assistant", "text": "Yes, teams can switch to annual billing. Would you like to talk it through with someone from our team?", "created_at": "2026-10-12T09:30:04.000Z" }, "actions": [ { "type": "searched_knowledge" }, { "type": "booking_offered", "booking_url": "https://..." } ] } ``` A turn can take several seconds while the agent searches and writes. Allow up to 60 seconds before timing out. ### Actions | `type` | What happened | What to do | | - | - | - | | `booking_offered` | The agent offered a meeting | If `booking_url` is set, show it as a button so the person can pick a time. It can be `null`, for example when your booking runs through a calendar connection whose time picker only works in the website chat; offer your own booking route then. | | `handoff_requested` | The person's request to talk to someone was recorded | Tell your team. The `handoff.requested` [webhook](/api-reference/webhooks) fires too. | | `searched_knowledge` | The agent looked something up in your knowledge | Nothing; useful for logging | ## Stream the reply Send `"stream": true` to receive the reply as it is written, as server-sent events (`Content-Type: text/event-stream`). The stream is the [Vercel AI SDK](https://ai-sdk.dev) UI message stream, version 1: each event is a `data:` line with a JSON chunk, and the stream ends with `data: [DONE]`. Chunks with `"type": "text-delta"` carry the next piece of text in `delta`. ```javascript theme={"theme":"vesper"} const res = await fetch(`https://api.meetdarwin.ai/v1/chat/sessions/${sessionId}/messages`, { method: "POST", headers: { Authorization: `Bearer ${process.env.DARWIN_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ text: userMessage, stream: true }), }); const decoder = new TextDecoder(); let buffer = ""; for await (const bytes of res.body) { buffer += decoder.decode(bytes, { stream: true }); const events = buffer.split("\n\n"); buffer = events.pop(); for (const event of events) { const data = event.replace(/^data:\s*/, ""); if (!data || data === "[DONE]") continue; const chunk = JSON.parse(data); if (chunk.type === "text-delta") process.stdout.write(chunk.delta); } } ``` A streamed reply does not include the `actions` summary; tool calls arrive as their own chunks. If you need the actions, use the JSON reply, or read the session afterwards. ## Read a session `GET /v1/chat/sessions/{id}` returns the session with its whole transcript, as in the create response, and a `status`: | `status` | Meaning | | - | - | | `open` | The conversation is going | | `handed_off` | The person asked for someone from your team | | `booked` | A meeting was booked in this conversation | ## Monthly allowance Each workspace can start 2,500 chat API sessions each calendar month, on every plan. On Agency, each client workspace has its own 2,500. Messages within a session are not counted. Legacy plans keep their allowance: Origin none, Adapt 500, Evolve 2,500 and Apex 10,000. When the allowance is used, creating a session returns `429` [`quota_exceeded`](/api-reference/errors#quota_exceeded), with `Retry-After` set to the start of the next month. Sessions already open keep working. Meetings booked and requests sent through the chat API use credits like any other. See [Plans and credits](https://www.meetdarwin.ai/help/account-and-billing/plans-and-credits). Each message is also a heavy request for [per-minute rate limits](/api-reference/rate-limits): 30 a minute per key on every plan. ## Errors | Status and code | When | | - | - | | `403` `feature_disabled` | The chat API isn't switched on | | `404` `not_found` | No session with that id in this workspace | | `402` `payment_required` | The workspace's free credits are used and no card is on file, so the chat is paused | | `409` `conflict` | The chat is switched off for the workspace | | `429` `rate_limited` | The per-minute limit is reached | | `429` `quota_exceeded` | The month's allowance of chat API conversations is used | | `502` `internal_error` | The agent could not answer this turn. Retry the message. | See [Errors](/api-reference/errors) for every code. # Retrieve a chat session and its transcript Source: https://docs.meetdarwin.ai/api-reference/chat/retrieve-a-chat-session-and-its-transcript /api-reference/openapi.json get /chat/sessions/{id} Requires the `chat:write` scope. # Send the visitor's message and get the agent's reply Source: https://docs.meetdarwin.ai/api-reference/chat/send-the-visitors-message-and-get-the-agents-reply /api-reference/openapi.json post /chat/sessions/{id}/messages Requires the `chat:write` scope. With `stream: true` the answer is `text/event-stream` in the AI SDK UI message stream format: `data:` lines with chunks such as `{"type":"text-delta","delta":"…"}`, ending with `data: [DONE]`. A streamed answer is never stored for Idempotency-Key replay. # Start a conversation with the agent Source: https://docs.meetdarwin.ai/api-reference/chat/start-a-conversation-with-the-agent /api-reference/openapi.json post /chat/sessions Requires the `chat:write` scope. # Create or update a contact by email Source: https://docs.meetdarwin.ai/api-reference/contacts/create-or-update-a-contact-by-email /api-reference/openapi.json post /contacts Requires the `contacts:write` scope. # Erase a contact and everything that names them Source: https://docs.meetdarwin.ai/api-reference/contacts/erase-a-contact-and-everything-that-names-them /api-reference/openapi.json delete /contacts/{id} Requires the `privacy:write` scope. # List contacts Source: https://docs.meetdarwin.ai/api-reference/contacts/list-contacts /api-reference/openapi.json get /contacts Requires the `contacts:read` scope. # Retrieve a contact Source: https://docs.meetdarwin.ai/api-reference/contacts/retrieve-a-contact /api-reference/openapi.json get /contacts/{id} Requires the `contacts:read` scope. # Update a contact Source: https://docs.meetdarwin.ai/api-reference/contacts/update-a-contact /api-reference/openapi.json patch /contacts/{id} Requires the `contacts:write` scope. # List conversations Source: https://docs.meetdarwin.ai/api-reference/conversations/list-conversations /api-reference/openapi.json get /conversations Requires the `conversations:read` scope. # Retrieve a conversation Source: https://docs.meetdarwin.ai/api-reference/conversations/retrieve-a-conversation /api-reference/openapi.json get /conversations/{id} Requires the `conversations:read` scope. # API errors Source: https://docs.meetdarwin.ai/api-reference/errors Every Darwin API error code, what it means and what to do: the RFC 9457 problem document, HTTP statuses, request ids and which errors are safe to retry. Every error the Darwin API returns has the same shape: an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem document with `Content-Type: application/problem+json`. Branch your code on `code`, show `detail` to a person, and log `request_id`. ```json theme={"theme":"vesper"} { "type": "https://docs.meetdarwin.ai/api-reference/errors#insufficient_scope", "title": "Insufficient scope", "status": 403, "code": "insufficient_scope", "detail": "This key needs the contacts:read scope.", "request_id": "req_8m2k4p0q1x7z9c3v5b6n" } ``` | Field | What it holds | | - | - | | `type` | A link to this page at the code's own section | | `title` | A short, fixed name for the code | | `status` | The HTTP status, repeated | | `code` | The stable, machine-readable reason. New codes may be added; existing ones do not change meaning. | | `detail` | A sentence for a person, specific to this request. Its wording can change; don't parse it. | | `param` | When one input caused the error, its name, for example `limit`, `email`, `buttons[0].label` or the `Idempotency-Key` header | | `required_plan`, `feature`, `upgrade_url` | Only with [`plan_required`](#plan_required): the plan that includes what was refused, what was refused, and where to upgrade | | `request_id` | The same id as the `X-Request-Id` header. Quote it to Darwin support. | ## Retry or fix? | Status | Retry? | | - | - | | `400`, `401`, `402`, `403`, `404`, `405`, `413`, `422` | No. Fix the request, key, workspace or plan first. | | `409` | Depends on the code: retry `idempotency_key_in_use` after a few seconds; re-read and resend for `conflict`; add another calendar or form first for `goal_required`. | | `429` | Yes, after the `Retry-After` header's seconds (`rate_limited`); `quota_exceeded` waits for the next month. | | `500`, `502` | Yes, with backoff, and with the same `Idempotency-Key` on writes. | ## Error codes ### authentication\_required **401.** The request had no `Authorization: Bearer …` header, or it was not in that form. Send your key as `Authorization: Bearer dk_live_…`. A key in a query string or another header is ignored. ### invalid\_api\_key **401.** The key is not valid: it was revoked, mistyped, cut short, or belongs to an AI app connection that expired. Check the key's first characters against the list in **Settings → Integrations → API**. If it shows **Revoked**, or you can't find it, create a new key. Darwin answers revoked, expired and unknown keys the same way on purpose. ### insufficient\_scope **403.** The key is valid but lacks the scope this endpoint needs. `detail` and the `WWW-Authenticate` header name the scope. Create a key with that scope in **Settings → Integrations → API** (scopes on an existing key can't be changed). See [Scopes](/api-reference/authentication#scopes). ### payment\_required **402.** The workspace has to finish adding a card before the API can be used, for example because Growth or Agency was chosen at sign-up and checkout was never completed. The chat API also returns it when the workspace's free credits are used and no card is on file. An owner signs in to Darwin and completes the card step. Retrying before then returns the same error. ### invalid\_request **400** (or **413** when the body is too large). The request could not be accepted as sent: invalid JSON, an unknown or missing field, a value out of range, an unknown event type, a malformed `starting_after` cursor, a bad date in `updated_since`, an `Idempotency-Key` that is too long, or a field that breaks a rule such as a webhook URL that isn't HTTPS. Read `detail` and `param`, which name the input and the rule. Request bodies are strict: a field the endpoint does not take is refused rather than ignored, so a typo fails loudly. ### not\_found **404.** No object with that id exists in this workspace. It may have been deleted or erased, the id may belong to another workspace, or it may be the wrong kind of id (a `ct_` contact id where a `conv_` conversation id belongs). Check the id and the key's workspace with `GET /me`. ### plan\_required **403.** The workspace's plan doesn't include what the request asks for. For example: * An outcome form with more than one qualifying question, a question with a fit rule, on Free (`feature` is `fit_rules`). ```json theme={"theme":"vesper"} { "type": "https://docs.meetdarwin.ai/api-reference/errors#plan_required", "title": "Needs a different plan", "status": 403, "code": "plan_required", "detail": "More than one qualifying question per form is on Growth and up.", "required_plan": "growth", "feature": "fit_rules", "upgrade_url": "https://app.meetdarwin.ai/admin/settings/billing?upgrade=growth", "request_id": "req_8m2k4p0q1x7z9c3v5b6n" } ``` Three more fields come with this code: | Field | What it holds | | - | - | | `required_plan` | The plan that includes what was refused: `growth` or `agency` | | `feature` | What was refused, such as `fit_rules` | | `upgrade_url` | A link to **Billing** in Darwin that opens the upgrade window for that plan | A workspace owner opens `upgrade_url`, or **Billing** in Darwin, and upgrades. See [Plans and credits](https://www.meetdarwin.ai/help/account-and-billing/plans-and-credits). While a paid plan's payment has failed, the workspace has Free's limits, so this code can appear until the card works. ### goal\_required **409.** The change would leave a live chat with nowhere to send visitors: it removes or switches off the last calendar, booking link or live outcome form while Darwin is live. The `title` is "Darwin needs a calendar or a live form". Add another calendar or form first, or switch the chat off with `PATCH /v1/agent` and `{"live": false}`. See [Go live](https://www.meetdarwin.ai/help/set-up-on-your-own/go-live). ### conflict **409.** The request clashes with the current state. Examples: * Another contact already has this `external_id`. * `PATCH /appearance` sent a `revision` that is no longer current, because someone saved the appearance since you read it. * The chat cannot be switched on yet, or the chat API was used while the chat is switched off. * The workspace already has the maximum number of chat buttons. Read the object again, resolve the clash, then send the request again. ### idempotency\_key\_reused **422.** This `Idempotency-Key` was already used for a different request (a different method, path or body). Use a new key for each distinct request. Reuse a key only to retry exactly the same request. See [Idempotency](/api-reference/idempotency). ### idempotency\_key\_in\_use **409.** A request with this `Idempotency-Key` is still being processed. Wait a few seconds and retry with the same key. You will get the first request's response once it finishes. ### rate\_limited **429.** The key made more requests in the current minute than its plan allows. Wait for the number of seconds in `Retry-After`, then retry. See [Rate limits](/api-reference/rate-limits). ### quota\_exceeded **429.** The workspace has used its monthly [chat API](/api-reference/chat-api#monthly-allowance) conversations. `Retry-After` gives the seconds until the next calendar month, when the allowance starts again. Do not retry before then. ### unsupported\_version **400.** The `Darwin-Version` header names a version this API does not have. Send `Darwin-Version: 2026-10-12` or leave the header out. See [Versioning](/api-reference/introduction#versioning). ### feature\_disabled **403.** The feature is not switched on for Darwin as a whole. Today this is the [chat API](/api-reference/chat-api), which Darwin enables on request. A feature your plan doesn't include returns [`plan_required`](#plan_required) instead. [Contact Darwin](https://www.meetdarwin.ai/help/support/contact-support) to ask for it. Retrying won't help. ### unprocessable **422.** The request is well formed but a product rule refuses it. For example, applying an opening as an experiment's winner before it has the evidence Darwin requires, or pausing or resuming a test whose state doesn't allow it. `detail` says which rule. These are the same rules the buttons in **Experiments** follow. ### internal\_error **500** (or **502** when the agent could not answer a chat API message). Something went wrong on Darwin's side. Retry with backoff. On a write, send the same `Idempotency-Key` so a retry can't apply twice; a failed request's key is released so the retry runs. If it keeps happening, [contact Darwin](https://www.meetdarwin.ai/help/support/contact-support) with the `request_id`. ## Errors outside the API * **Webhooks**: Darwin records your endpoint's answer, not an error code. See [Deliveries](/api-reference/webhooks/delivery#check-deliveries-and-send-again). * **MCP**: tool failures come back as a tool result with `isError: true` and the same sentence you would see in `detail`. A plan limit names the plan that has the feature and links to **Billing**. See [MCP](/api-reference/mcp). # List events from the last 30 days Source: https://docs.meetdarwin.ai/api-reference/events/list-events-from-the-last-30-days /api-reference/openapi.json get /events Requires the `events:read` scope. # Retrieve an event Source: https://docs.meetdarwin.ai/api-reference/events/retrieve-an-event /api-reference/openapi.json get /events/{id} Requires the `events:read` scope. # Add your own opening to a test Source: https://docs.meetdarwin.ai/api-reference/experiments/add-your-own-opening-to-a-test /api-reference/openapi.json post /experiments/{id}/variants Requires the `experiments:write` scope. # Apply an opening as the winner Source: https://docs.meetdarwin.ai/api-reference/experiments/apply-an-opening-as-the-winner /api-reference/openapi.json post /experiments/{id}/promote Requires the `experiments:write` scope. # Change an opening's words Source: https://docs.meetdarwin.ai/api-reference/experiments/change-an-openings-words /api-reference/openapi.json patch /experiments/{id}/variants/{variantId} Requires the `experiments:write` scope. On a running test the new words start as a fresh opening (a new id) and the old one's results are kept as history; changing the current opening swaps it the same way. # Give a page or section its own opening Source: https://docs.meetdarwin.ai/api-reference/experiments/give-a-page-or-section-its-own-opening /api-reference/openapi.json post /page_openings Requires the `experiments:write` scope. Replaces the page's opening for every visitor at once, without a test. A page with a one-page greeting of its own gets the same words on the greeting. Refused (409) while a test runs on that page or its greeting, because the test compares against the page's current opening. overridden_by names any other greeting visitors still get. # List opening tests with their results Source: https://docs.meetdarwin.ai/api-reference/experiments/list-opening-tests-with-their-results /api-reference/openapi.json get /experiments Requires the `experiments:read` scope. # List pages with an opening of their own Source: https://docs.meetdarwin.ai/api-reference/experiments/list-pages-with-an-opening-of-their-own /api-reference/openapi.json get /page_openings Requires the `experiments:read` scope. Every other page shows the site's opening (GET /agent). Each names the page's own greeting and any greeting ordinary visitors get instead (overridden_by); greetings come before page openings. # Pause a test Source: https://docs.meetdarwin.ai/api-reference/experiments/pause-a-test /api-reference/openapi.json post /experiments/{id}/pause Requires the `experiments:write` scope. # Put a page back on the site's opening Source: https://docs.meetdarwin.ai/api-reference/experiments/put-a-page-back-on-the-sites-opening /api-reference/openapi.json delete /page_openings/{id} Requires the `experiments:write` scope. Refused (409) while a test runs on that page. # Resume a paused test Source: https://docs.meetdarwin.ai/api-reference/experiments/resume-a-paused-test /api-reference/openapi.json post /experiments/{id}/resume Requires the `experiments:write` scope. # Retrieve a page's opening Source: https://docs.meetdarwin.ai/api-reference/experiments/retrieve-a-pages-opening /api-reference/openapi.json get /page_openings/{id} Requires the `experiments:read` scope. # Retrieve a test with its results worked out now Source: https://docs.meetdarwin.ai/api-reference/experiments/retrieve-a-test-with-its-results-worked-out-now /api-reference/openapi.json get /experiments/{id} Requires the `experiments:read` scope. `live` has each opening's views, conversations, visitors who reached booking, meetings and chance to beat the current opening, as the Experiments screen shows them. Reading it saves nothing. # Start a test with your own openings Source: https://docs.meetdarwin.ai/api-reference/experiments/start-a-test-with-your-own-openings /api-reference/openapi.json post /experiments Requires the `experiments:write` scope. Test one page (page), a unit (site, a page type or a page group) or a greeting (gr_…). A page with a live one-page greeting of its own is tested on that greeting, which is what its visitors see. Darwin adds the opening the page shows now as the current one, so your openings are compared with what visitors see today. One test per page or unit at a time. Starts at once unless launch is false. # Stop a test Source: https://docs.meetdarwin.ai/api-reference/experiments/stop-a-test /api-reference/openapi.json post /experiments/{id}/stop Requires the `experiments:write` scope. # Take an opening out of a test Source: https://docs.meetdarwin.ai/api-reference/experiments/take-an-opening-out-of-a-test /api-reference/openapi.json delete /experiments/{id}/variants/{variantId} Requires the `experiments:write` scope. Its history is kept. The current opening and the last new one cannot be removed; stop the test instead. # What one page shows now, and why Source: https://docs.meetdarwin.ai/api-reference/experiments/what-one-page-shows-now-and-why /api-reference/openapi.json get /page_openings/current Requires the `experiments:read` scope. # Archive a greeting Source: https://docs.meetdarwin.ai/api-reference/greetings/archive-a-greeting /api-reference/openapi.json delete /greetings/{id} Requires the `agent:write` scope. It stops greeting anyone and any test on it ends, results kept; its visitors get the next greeting that fits. The website default can't be archived. # Change a greeting Source: https://docs.meetdarwin.ai/api-reference/greetings/change-a-greeting /api-reference/openapi.json patch /greetings/{id} Requires the `agent:write` scope. Omit what should stay. Changing the words, buttons or audience of a greeting under test is refused (409) unless end_test is true, which ends the test first and keeps its results. status live restores an archived greeting. The website default takes words, buttons and testing, not an audience. # Create a greeting Source: https://docs.meetdarwin.ai/api-reference/greetings/create-a-greeting /api-reference/openapi.json post /greetings Requires the `agent:write` scope. type page needs page; type audience needs show_to; type link takes links (lk_…). Live at once unless status is draft. Changes the live website. # List greetings Source: https://docs.meetdarwin.ai/api-reference/greetings/list-greetings /api-reference/openapi.json get /greetings Requires the `agent:read` scope. What the chat says first and to whom: one-page greetings, audience greetings, personal link greetings and the website default (the catch-all). A live greeting comes before a page's opening. Archived greetings are listed last. # Retrieve a greeting Source: https://docs.meetdarwin.ai/api-reference/greetings/retrieve-a-greeting /api-reference/openapi.json get /greetings/{id} Requires the `agent:read` scope. # Retry writes safely with Idempotency-Key Source: https://docs.meetdarwin.ai/api-reference/idempotency Send an Idempotency-Key header on Darwin API writes so a retried request after a timeout never creates a second contact, document or webhook endpoint. A network can fail after Darwin has done the work but before you get the answer. Send an `Idempotency-Key` header on every write (`POST`, `PATCH`, `DELETE`) and you can retry without doing it twice. ```bash theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/knowledge/documents \ -H "Authorization: Bearer $DARWIN_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6c1d0b2e-8f4a-4f3e-9d61-0b7a2e5c9f10" \ -d '{"name": "Refund policy", "content": "Refunds are available within 30 days of purchase for any annual plan. Contact billing to start one."}' ``` ## How it works * The first request with a key runs normally. Darwin stores its response for **24 hours**. * A retry with the same key, method, path and body gets the stored response back, with the same status and body, plus the header `Idempotent-Replayed: true`. Nothing runs a second time. * A refused request (a `4xx`) is a final answer and is replayed like a success. Fix the request and send it with a **new** key. * A Darwin-side failure (a `5xx`) is not stored, so a retry with the same key runs the request again. * While the first request is still running, a retry with the same key gets `409` [`idempotency_key_in_use`](/api-reference/errors#idempotency_key_in_use). Wait a few seconds and retry. * The same key with a different method, path or body gets `422` [`idempotency_key_reused`](/api-reference/errors#idempotency_key_reused). Keys are scoped to the API key that sent them, and must be 1 to 255 visible ASCII characters. A UUID v4 per logical operation is a good choice. ## Retry pattern ```javascript Node theme={"theme":"vesper"} import { randomUUID } from "node:crypto"; async function createContact(contact) { const key = randomUUID(); // one key for this contact, reused on every retry for (let attempt = 0; attempt < 5; attempt++) { try { const res = await fetch("https://api.meetdarwin.ai/v1/contacts", { method: "POST", headers: { Authorization: `Bearer ${process.env.DARWIN_API_KEY}`, "Content-Type": "application/json", "Idempotency-Key": key, }, body: JSON.stringify(contact), }); if (res.status === 429 || res.status === 409 || res.status >= 500) { const wait = Number(res.headers.get("retry-after")) || 2 ** attempt; await new Promise((r) => setTimeout(r, wait * 1000)); continue; } return await res.json(); } catch { await new Promise((r) => setTimeout(r, 2 ** attempt * 1000)); // network error: retry with the same key } } throw new Error("Darwin did not answer after 5 attempts"); } ``` ```python Python theme={"theme":"vesper"} import os, time, uuid, requests def create_contact(contact): key = str(uuid.uuid4()) # one key for this contact, reused on every retry for attempt in range(5): try: res = requests.post( "https://api.meetdarwin.ai/v1/contacts", json=contact, headers={ "Authorization": f"Bearer {os.environ['DARWIN_API_KEY']}", "Idempotency-Key": key, }, timeout=30, ) except requests.RequestException: time.sleep(2 ** attempt) continue if res.status_code in (409, 429) or res.status_code >= 500: time.sleep(int(res.headers.get("Retry-After", 2 ** attempt))) continue return res.json() raise RuntimeError("Darwin did not answer after 5 attempts") ``` A `409` with the code `conflict` is not a retry case: read the object again and resolve it. The loop above retries every `409` for brevity; check `code` in production code. ## Writes that are safe without a key Some writes give the same result however often you send them: `PATCH` with the same fields, or `DELETE` of something already gone (it returns `404`). `POST /contacts` is an upsert by email, so a repeat updates the same contact. A key still protects you from a retry racing the original, so sending one on every write is the simplest rule. # Darwin API Source: https://docs.meetdarwin.ai/api-reference/introduction Use the Darwin API to read conversations, contacts, meetings and analytics, manage knowledge and the agent, and receive signed webhooks. Base URL, keys, scopes and versioning. **Find it in Darwin** Settings → Integrations → API [Open this screen ↗](https://app.meetdarwin.ai/admin/settings/integrations/api) The Darwin API lets your own code and tools work with your workspace. Everything you can see in Darwin is available as JSON, and the changes you make on the Agent, Knowledge and Experiments pages can be made from code. ## What you can build | You want to | Use | | - | - | | Send leads, meetings and requests for a person to your CRM, Slack or a warehouse as they happen | [Webhooks](/api-reference/webhooks) | | Sync conversations, contacts and meetings on a schedule | REST: `GET /conversations`, `/contacts`, `/meetings` with `updated_since` | | Report on the funnel, pages, sources and conversation quality | REST: `GET /analytics` | | Keep the agent's knowledge current from your CMS or help center | REST: `/knowledge/sources` and `/knowledge/documents` | | Change the agent, its look, its chat buttons and its opening tests | REST: `/agent`, `/appearance`, `/chat_buttons`, `/experiments` | | Open the chat, recognise signed-in visitors and track bookings on your website | [Browser SDK](/api-reference/browser-sdk) | | Let Claude, ChatGPT or Cursor read results and manage the agent | [MCP server](/api-reference/mcp) | | Answer a person's request to see or delete their data | [Privacy](/api-reference/privacy) | The API, the MCP server and the [chat API](/api-reference/chat-api) are on every plan, Free included. Plans differ in [rate limits](/api-reference/rate-limits). ## Base URL ```text theme={"theme":"vesper"} https://api.meetdarwin.ai/v1 ``` **Settings → Integrations → API** shows the same API at `https://app.meetdarwin.ai/api/v1`. Both addresses reach the same endpoints; examples in these docs use the first. The machine-readable [OpenAPI 3.1 spec](https://app.meetdarwin.ai/api/v1/openapi.json) describes every endpoint and is what the endpoint pages in this section are built from. ## Authenticate Every request carries a secret key in the `Authorization` header: ```http theme={"theme":"vesper"} Authorization: Bearer dk_live_... ``` **1. Open Settings → Integrations → API** Open **Settings** at the bottom of the sidebar, choose **Integrations**, then **API**. You need to be an owner or admin to create keys; members can see this screen but not change it. **2. Create a key** Select **Create key**. Name it after where it will be used, for example **Zapier** or **Warehouse sync**, so you know what stops working if you revoke it. **3. Choose its access** Pick **Read only** (every read scope), **Full access** (every scope) or **Choose** to tick individual [scopes](/api-reference/authentication#scopes). Give a key only what its job needs. **4. Copy it now** Darwin shows the key once. Store it where your code reads secrets, such as an environment variable or a secrets manager. Darwin keeps only a fingerprint, so a lost key cannot be shown again; create a new one instead. More on prefixes, scopes, revoking and storage: [Authentication](/api-reference/authentication). ## Make your first request `GET /me` needs no scope and tells you which workspace and scopes a key has. It is the quickest way to check a key works. ```bash curl theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/me \ -H "Authorization: Bearer $DARWIN_API_KEY" ``` ```javascript Node theme={"theme":"vesper"} const res = await fetch("https://api.meetdarwin.ai/v1/me", { headers: { Authorization: `Bearer ${process.env.DARWIN_API_KEY}` }, }); console.log(res.status, await res.json()); ``` ```python Python theme={"theme":"vesper"} import os, requests res = requests.get( "https://api.meetdarwin.ai/v1/me", headers={"Authorization": f"Bearer {os.environ['DARWIN_API_KEY']}"}, ) print(res.status_code, res.json()) ``` **It worked if** you get `200` and a body like this: ```json theme={"theme":"vesper"} { "object": "api_key_identity", "key": "key_4k2m9q1x7z3c5v8b", "name": "Warehouse sync", "kind": "secret_key", "mode": "live", "scopes": ["analytics:read", "contacts:read", "conversations:read", "meetings:read"], "workspace": "6f1c2b9e-4d1a-4f6b-9a3e-2c7d8e9f0a1b" } ``` Then list your five most recent conversations: ```bash theme={"theme":"vesper"} curl "https://api.meetdarwin.ai/v1/conversations?limit=5" \ -H "Authorization: Bearer $DARWIN_API_KEY" ``` A `401` means the key is missing or wrong; a `403` with `insufficient_scope` means the key lacks `conversations:read`. See [Errors](/api-reference/errors). ## Requests and responses * Send and receive JSON. Request bodies use `Content-Type: application/json`; bodies are limited to 256 KB unless an endpoint says otherwise. * Times are ISO 8601 in UTC, for example `2026-10-12T09:30:00.000Z`. * Every object has a prefixed id that tells you what it is: `conv_` conversation, `ct_` contact, `mtg_` meeting, `evt_` event, `we_` webhook endpoint, `src_` knowledge page, `doc_` document, `exp_` opening test, `btn_` chat button, `chs_` chat session. * Lists share one shape and use [cursor pagination](/api-reference/pagination). * Writes can be retried safely with an [Idempotency-Key](/api-reference/idempotency). * Conversation and meeting lists leave out your own preview chats unless you pass `include_test=true`. Nothing the API does counts as a website visitor in your analytics. ## Request ids Every response, success or error, carries an `X-Request-Id` header such as `req_8m2k4p0q1x7z9c3v5b6n`. Errors repeat it as `request_id` in the body. Log it; it is the fastest way for Darwin support to find a call. **Settings → Integrations → API → Recent requests** shows the last 50 calls from any key in the past 30 days, with status, error code, duration and the key that made them. ## Versioning The API is versioned by date. The current version is `2026-10-12`, and every response says which version answered in a `Darwin-Version` header. Webhook payloads carry the same value as `api_version`. You can pin a version by sending the header yourself: ```http theme={"theme":"vesper"} Darwin-Version: 2026-10-12 ``` Pinning is optional today because there is only one version. When a later version changes a response shape, a pinned integration keeps getting the shape it was built for. Darwin adds new fields and new endpoints without a new version, so write clients that ignore fields they don't know. A version Darwin does not recognise returns `400` with the code `unsupported_version`. ## Next > [card] [Authentication and scopes](/api-reference/authentication) > > Key types, every scope and how to revoke a key. > [card] [Webhooks](/api-reference/webhooks) > > Events, payloads, signatures and retries. > [card] [Browser SDK](/api-reference/browser-sdk) > > Control the chat and listen to it from your page. > [card] [Recipes](/api-reference/recipes) > > Slack alerts, CRM sync and a nightly warehouse export. # Add a document from text or Markdown Source: https://docs.meetdarwin.ai/api-reference/knowledge/add-a-document-from-text-or-markdown /api-reference/openapi.json post /knowledge/documents Requires the `knowledge:write` scope. # Add website pages Source: https://docs.meetdarwin.ai/api-reference/knowledge/add-website-pages /api-reference/openapi.json post /knowledge/sources Requires the `knowledge:write` scope. # Delete a document Source: https://docs.meetdarwin.ai/api-reference/knowledge/delete-a-document /api-reference/openapi.json delete /knowledge/documents/{id} Requires the `knowledge:write` scope. # List documents Source: https://docs.meetdarwin.ai/api-reference/knowledge/list-documents /api-reference/openapi.json get /knowledge/documents Requires the `knowledge:read` scope. # List website pages the agent answers from Source: https://docs.meetdarwin.ai/api-reference/knowledge/list-website-pages-the-agent-answers-from /api-reference/openapi.json get /knowledge/sources Requires the `knowledge:read` scope. # Read a page again now Source: https://docs.meetdarwin.ai/api-reference/knowledge/read-a-page-again-now /api-reference/openapi.json post /knowledge/sources/{id}/refresh Requires the `knowledge:write` scope. # Replace a document's text Source: https://docs.meetdarwin.ai/api-reference/knowledge/replace-a-documents-text /api-reference/openapi.json patch /knowledge/documents/{id} Requires the `knowledge:write` scope. # Retrieve a document with its text Source: https://docs.meetdarwin.ai/api-reference/knowledge/retrieve-a-document-with-its-text /api-reference/openapi.json get /knowledge/documents/{id} Requires the `knowledge:read` scope. # Retrieve a page Source: https://docs.meetdarwin.ai/api-reference/knowledge/retrieve-a-page /api-reference/openapi.json get /knowledge/sources/{id} Requires the `knowledge:read` scope. # Stop answering from a page Source: https://docs.meetdarwin.ai/api-reference/knowledge/stop-answering-from-a-page /api-reference/openapi.json delete /knowledge/sources/{id} Requires the `knowledge:write` scope. # Connect Claude, ChatGPT or Cursor (MCP) Source: https://docs.meetdarwin.ai/api-reference/mcp Connect Claude, ChatGPT, Claude Code or Cursor to Darwin's MCP server to read results, find unanswered questions, add knowledge, and manage the agent, its forms, chat buttons and personal links. Setup, tools, view-only access and disconnecting. **Find it in Darwin** Settings → Integrations → API → AI assistants [Open this screen ↗](https://app.meetdarwin.ai/admin/settings/integrations/api) Darwin runs a [Model Context Protocol](https://modelcontextprotocol.io) server, so the AI assistant you already use can work with your workspace. Ask it things like *"How did the chat do this week compared with last?"*, *"What did visitors ask that the agent couldn't answer?"*, *"Add our new refund policy to the agent's knowledge"*, *"Make personal links for these 40 prospects"*, *"Add a callback form for people who'd rather talk on the phone"*, *"Give our pricing page its own greeting"* or *"Test a new opening on our pricing page and tell me how it's doing on Friday."* | | | | - | - | | Server URL | `https://app.meetdarwin.ai/api/mcp` | | Read-only server URL | `https://app.meetdarwin.ai/api/mcp/readonly` | | Transport | Streamable HTTP | | Sign-in | OAuth (Claude, ChatGPT) or an API key in an `Authorization` header (Claude Code, Cursor, scripts) | The read-only URL offers only the tools that read, whatever the connection is allowed to do. Not a developer? [Use Darwin in Claude or ChatGPT](https://www.meetdarwin.ai/help/ai-and-browser/ai-assistants) covers connecting and what to ask, in plain words. ## Connect your assistant > [tab] Claude > > **1. Add a custom connector** > > In Claude, open **Settings → Connectors** and add a custom connector. Name it **Darwin** and paste `https://app.meetdarwin.ai/api/mcp` as its URL. On a Team or Enterprise plan, an owner may need to add it for the organisation first. > > **2. Sign in to Darwin** > > Select **Connect**. Darwin's sign-in opens; sign in if you aren't already. The connection belongs to the **workspace you are signed in to**, so switch to the right workspace in Darwin first if you have several. > > **3. Review and allow** > > Darwin shows **Connect Claude to** your workspace. Check the workspace (select **Switch** to pick another one you own or administer), choose **Full access** or **View only**, and select **Connect Claude**. You return to Claude. > > Only an owner or admin can allow a connection. The request expires after 15 minutes; start again from Claude if it does. > > **4. Check it** > > Ask Claude: *"Using Darwin, how many meetings did the chat book in the last 7 days?"* **It worked if** Claude calls Darwin's `get_performance` or `get_overview` tool and answers with your numbers. In Darwin, the connection appears under **AI assistants**. > [tab] ChatGPT > > In ChatGPT, open **Plugins → + → Add custom MCP server**, enter `https://app.meetdarwin.ai/api/mcp` under **Connection**, choose OAuth, and select **Create as a plugin**. When ChatGPT sends you to Darwin, sign in, check the workspace, choose **Full access** or **View only**, and select **Connect ChatGPT**. [Step by step →](https://www.meetdarwin.ai/help/ai-and-browser/chatgpt) > [tab] Claude Code > > Create an API key in **Settings → Integrations → API → Create key** with the scopes you want the assistant to have (see [what each tool needs](/api-reference/mcp/tools)), then: > > ```bash theme={"theme":"vesper"} > claude mcp add --transport http darwin https://app.meetdarwin.ai/api/mcp \ > --header "Authorization: Bearer YOUR_API_KEY" > ``` > > **Settings → Integrations → API → AI assistants** shows this command ready to copy. To sign in with OAuth instead of a key, leave out `--header`, then run `/mcp` in Claude Code and choose Darwin to authenticate. > [tab] Cursor > > Add Darwin to `~/.cursor/mcp.json` (all projects) rather than a project's `.cursor/mcp.json`, so the key never lands in a repository: > > ```json theme={"theme":"vesper"} > { > "mcpServers": { > "darwin": { > "url": "https://app.meetdarwin.ai/api/mcp", > "headers": { "Authorization": "Bearer YOUR_API_KEY" } > } > } > } > ``` > > Restart Cursor, or reload MCP servers in its settings, and check that Darwin's tools are listed. > > The same URL and header work with any MCP client that supports Streamable HTTP, including the Claude Agent SDK. ## Next > [card] [Tools](/api-reference/mcp/tools) > > Every tool, what it does and the scope it needs. > [card] [Access and limits](/api-reference/mcp/access) > > What an assistant can never do, disconnecting, limits and fixes. # MCP access and limits Source: https://docs.meetdarwin.ai/api-reference/mcp/access What an AI assistant connected to Darwin over MCP can never do, how to disconnect one, rate and size limits, and fixes for common connection problems. ## What an assistant can never do Through MCP, an assistant cannot see or change billing, your team, API keys or webhook endpoints; cannot delete conversations; cannot erase or export a person's data; and cannot change experiment results. It can only act in the one workspace it was connected to. The chat on your website never gets these tools. ## Disconnect an assistant Open **Settings → Integrations → API → AI assistants**. Each OAuth connection shows the app's name, when it was connected and last used, and its access (**Full access** or **View only**). Select **Disconnect** and confirm. It loses access at once; connecting again needs a new sign-in. For Claude Code, Cursor or anything using an API key, revoke that key in **API keys**. An OAuth connection stays signed in while it is used. One that goes unused for 30 days has to sign in again. ## Limits Each tool call counts as one request against the connection's per-minute [rate limit](/api-reference/rate-limits). Tool results are capped at 200,000 characters. ## Troubleshooting > [accordion] The assistant says Darwin has no tool for what I asked > > The connection's access doesn't include that tool. A **View only** connection, the read-only URL, or a key without the scope hides it. Disconnect and connect again with **Full access**, or create a key with the scope. > [accordion] Only an owner or admin of this workspace can connect an app > > Ask an owner or admin of the workspace to connect it, or to change your role in **Team**. > [accordion] This request expired > > Consent requests last 15 minutes. Start connecting again from your assistant. > [accordion] Finish adding a card to this workspace > > The workspace has to complete its card step in Darwin before any API or MCP access works. # MCP tools Source: https://docs.meetdarwin.ai/api-reference/mcp/tools Every tool Darwin's MCP server offers Claude, ChatGPT, Claude Code and Cursor: the tools that read results, conversations and knowledge, the tools that change the agent, forms, links and opening tests, and the scope each needs. What a connection can use depends on its access. An API key gets the tools its [scopes](/api-reference/authentication#scopes) allow. An OAuth connection gets every read, plus changes to knowledge, the agent, chat buttons, outcome forms, greetings, personal links and opening tests, unless you chose **View only**. ### Tools that read | Tool | What it does | Needs | | - | - | - | | `get_overview` | The Home overview for today, 7 or 30 days: key numbers, what needs attention, test status and conversations worth reading | `analytics:read` | | `get_performance` | Visitors, chats, leads and meetings against the previous period, the funnel and its biggest drop, top pages and sources (AI assistants marked) and conversation quality | `analytics:read` | | `list_conversations` | Recent conversations with page, first message, outcome, grade and source; filter to leads, meetings or requests for a person | `conversations:read` | | `get_conversation` | One conversation in full: transcript, visitor, lead and Darwin's grades | `conversations:read` | | `find_unanswered_questions` | Conversations where the agent failed or didn't answer, with the visitor's question and what to add | `conversations:read` | | `list_meetings` | Meetings booked from the chat, with invitee, time and source conversation | `meetings:read` | | `list_outcome_requests` | Requests visitors sent through your [outcome forms](https://www.meetdarwin.ai/help/outcomes/what-are-outcomes), with every answer, the contact, fit and conversation; filter by form or fit | `meetings:read` | | `list_contacts` | People who gave their email or booked; look one up by email | `contacts:read` | | `list_knowledge` | The website pages (with read status) and documents the agent answers from | `knowledge:read` | | `list_experiments` | Opening tests with each opening's words and latest results | `experiments:read` | | `get_experiment` | One test with its results worked out now: each opening's views, visitors who reached booking, meetings and chance to beat the current opening, and what a winner still needs | `experiments:read` | | `get_page_opening` | What one page shows first now, and why: a greeting that speaks for the page, a running test, the page's own opening or the site's | `experiments:read` | | `list_page_openings` | Pages and sections with an opening of their own, each with the page's own greeting and any greeting visitors get instead | `experiments:read` | | `list_greetings` | Every greeting: one-page greetings, audience greetings, personal link greetings and the website default (the catch-all), with their words, buttons, who sees each, status and any test on it. Filter by type or status. | `agent:read` | | `get_greeting` | One greeting in full | `agent:read` | | `get_agent` | The agent's name, tone, greeting, booking link, business context, opening and whether the chat is live | `agent:read` | | `list_chat_buttons` | Your [chat buttons](/api-reference/browser-sdk#start-a-chat-from-your-own-button), each with its HTML to paste and a plain link | `agent:read` | | `list_outcomes` | Your [outcome forms](https://www.meetdarwin.ai/help/outcomes/what-are-outcomes) and **Book a meeting**: questions, fit rules, whether each is live, and its last 30 days | `agent:read` | | `list_personal_links` | Your [personal links](https://www.meetdarwin.ai/help/links/personal-links): page, greeting, the address with each email tool's merge fields, and visits, chats and meetings | `agent:read` | | `list_link_greetings` | Greetings made for personal links, as the links store them, with a filled-in preview, their buttons and note, and how many links use each. `list_greetings` shows every greeting. | `agent:read` | | `make_personal_urls` | Each person's own address for a personal link, details filled in, for up to 200 people, with the greeting each will see. Nothing is stored. | `agent:read` | | `list_link_visits` | Who opened a personal link, with the details the link carried, the page and any conversation they started | `analytics:read` | | `search_help` | Searches this documentation | `agent:read` | The conversation and meeting tools include visitors' names and emails only when the connection has the `contacts:read` scope, as the REST API does; without it, those fields are empty. ### Tools that change things Your assistant should ask you before using these. Darwin tells it which tools change the live website, and marks the ones that remove or switch something as destructive. | Tool | What it does | Changes what visitors see | Needs | | - | - | - | - | | `add_pages` | Adds up to 50 public website pages for the agent to learn from; read within minutes | Answers | `knowledge:write` | | `add_document` | Teaches the agent something that isn't on the website, from text or Markdown (at least 80 characters) | Answers | `knowledge:write` | | `remove_page` | Stops the agent answering from one page (destructive) | Answers | `knowledge:write` | | `refresh_page` | Reads one page again now | Answers | `knowledge:write` | | `update_agent` | Changes the agent's name, tone, greeting, suggestions, booking link, business context or opening | Yes | `agent:write` | | `update_appearance` | Changes colors, corners, text size, launcher icon, sounds and motion | Yes | `agent:write` | | `create_chat_button` | Makes a button for your website that opens the chat: **Send** starts the conversation with a message, **Fill in** types it for the visitor, **Agent first** has the agent speak first. Returns the HTML to paste and a plain link. | Once pasted | `agent:write` | | `update_chat_button` | Changes a button's words, behavior or look. A new message or behavior reaches buttons already on your website at once; a new label or look comes back as new HTML to paste over the old copy. | Yes | `agent:write` | | `delete_chat_button` | Deletes a button. Copies already on your website still open the chat but no longer start the conversation (destructive). | Yes | `agent:write` | | `create_outcome` | Builds an [outcome form](https://www.meetdarwin.ai/help/outcomes/build-an-outcome) from the quote or callback template or your own questions, with fit rules. Saved off unless asked to make it live. | Once live | `agent:write` | | `update_outcome` | Changes a form or switches it live or off. With the id `meeting`, sets **Book a meeting**'s questions before the calendar. | Yes | `agent:write` | | `delete_outcome` | Removes a form for good; its requests stay (destructive) | Yes | `agent:write` | | `create_link_greeting` | Writes a greeting for personal links: text with the person's first name, company or job title and words for when one is missing, up to four reply buttons, and a note for the agent | Once a link uses it | `agent:write` | | `update_link_greeting` | Changes a greeting; every link that uses it, and every email already sent, shows the new one | Yes | `agent:write` | | `delete_link_greeting` | Deletes a greeting; links that used it open the page without one until they get another (destructive) | Yes | `agent:write` | | `create_personal_link` | Makes a personal link: a name, the page it opens, and a greeting or a new one written in the same call | Once sent | `agent:write` | | `update_personal_link` | Changes a link's name, page or greeting | Yes | `agent:write` | | `delete_personal_link` | Deletes a link; people who open it later get your first greeting (destructive) | Yes | `agent:write` | | `set_chat_live` | Switches the chat on or off for every visitor (destructive) | Yes | `agent:write` | | `pause_experiment` | Pauses a running opening test; visitors see the current opening | Yes | `experiments:write` | | `resume_experiment` | Resumes a paused test | Yes | `experiments:write` | | `apply_winner` | Makes an opening the one every visitor sees. Refused unless it has the evidence Darwin requires (destructive). | Yes | `experiments:write` | | `add_opening` | Adds your own opening (a headline and up to four buttons) to a running test | Yes | `experiments:write` | | `create_experiment` | Starts a test of your own openings on a page, a page type, a page group, the whole site or one greeting, against what it shows now. A page with its own greeting is tested on that greeting. | Yes | `experiments:write` | | `edit_opening` | New words for an opening in a running test; they start as a fresh opening | Yes | `experiments:write` | | `remove_opening` | Takes an opening out of a running test (destructive) | Yes | `experiments:write` | | `stop_experiment` | Ends a test without applying a winner (destructive) | Yes | `experiments:write` | | `set_page_opening` | Gives a page or a section its own opening, without a test. A page with its own greeting gets the same words on it. Refused while a test runs there. The answer names any other greeting visitors still get. | Yes | `experiments:write` | | `reset_page_opening` | Puts a page back on the site's opening and names any greeting that still speaks for it (destructive) | Yes | `experiments:write` | | `create_greeting` | Makes a greeting for one page, an audience (campaign, source, referrer, device, country, new or returning visitors) or personal links, with up to four reply buttons. Live at once unless saved as a draft. | Yes | `agent:write` | | `update_greeting` | Changes a greeting, the website default included: words, buttons, who sees it, status. A greeting under test changes only with `end_test`, which ends the test and keeps its results. | Yes | `agent:write` | | `archive_greeting` | Archives a greeting; any test on it ends, results kept. `update_greeting` with status `live` restores it (destructive). | Yes | `agent:write` | | `set_test_automation` | Chooses whether Darwin starts new tests and applies winners on its own; Darwin keeps scoring every test | Yes | `agent:write` | Changes take effect exactly as if you made them on the Agent, Knowledge, Greetings or Experiments pages, with the same rules. A live greeting comes before a page's opening, so `get_page_opening` is the way to check what a page really shows. An opening an assistant adds carries the hypothesis *Added by an AI assistant over MCP.* # List meetings booked from the chat Source: https://docs.meetdarwin.ai/api-reference/meetings/list-meetings-booked-from-the-chat /api-reference/openapi.json get /meetings Requires the `meetings:read` scope. # Retrieve a meeting Source: https://docs.meetdarwin.ai/api-reference/meetings/retrieve-a-meeting /api-reference/openapi.json get /meetings/{id} Requires the `meetings:read` scope. # Change an outcome form, or switch it live or off Source: https://docs.meetdarwin.ai/api-reference/outcomes/change-an-outcome-form-or-switch-it-live-or-off /api-reference/openapi.json patch /outcomes/{id} Requires the `agent:write` scope. Send only what changes; `questions` replaces the list, so send each kept question with its id. `meeting` as the id sets Book a meeting's questions (asked before the calendar), phone step and not-a-fit message; it is live while it has questions. # Create an outcome form Source: https://docs.meetdarwin.ai/api-reference/outcomes/create-an-outcome-form /api-reference/openapi.json post /outcomes Requires the `agent:write` scope. Start from a template (quote, callback) or your own questions. A new form is a draft unless status is live. A request a visitor sends through a live form counts as a credit, like a booked meeting, when it fits; one that fails the fit rules costs nothing. Fit rules on more than one question per form need Growth (403 `plan_required`). # List requests sent through outcome forms Source: https://docs.meetdarwin.ai/api-reference/outcomes/list-requests-sent-through-outcome-forms /api-reference/openapi.json get /outcome_requests Requires the `meetings:read` scope. Names, emails and phone numbers need contacts:read. # List the agent's outcome forms, their answer keys and results Source: https://docs.meetdarwin.ai/api-reference/outcomes/list-the-agents-outcome-forms-their-answer-keys-and-results /api-reference/openapi.json get /outcomes Requires the `agent:read` scope. Book a meeting is listed once it has been given questions; GET /outcomes/meeting always answers. # Remove an outcome form Source: https://docs.meetdarwin.ai/api-reference/outcomes/remove-an-outcome-form /api-reference/openapi.json delete /outcomes/{id} Requires the `agent:write` scope. Requests already sent stay. To stop offering a form for now, set status to draft instead. Book a meeting cannot be removed. # Retrieve an outcome form Source: https://docs.meetdarwin.ai/api-reference/outcomes/retrieve-an-outcome-form /api-reference/openapi.json get /outcomes/{id} Requires the `agent:read` scope. `meeting` as the id is the built-in Book a meeting. # Retrieve an outcome request Source: https://docs.meetdarwin.ai/api-reference/outcomes/retrieve-an-outcome-request /api-reference/openapi.json get /outcome_requests/{id} Requires the `meetings:read` scope. # Pagination and syncing Source: https://docs.meetdarwin.ai/api-reference/pagination Page through Darwin API lists with limit, starting_after and next_cursor, and sync only what changed with updated_since and created_since. Every list endpoint returns the same envelope and pages with a cursor. Cursors stay correct while new conversations arrive; page offsets would shift. ```json theme={"theme":"vesper"} { "object": "list", "data": [ { "id": "conv_c-1760280000000-k3j9", "object": "conversation" } ], "has_more": true, "next_cursor": "eyJ0IjoiMjAyNi0xMC0xMlQwOTozMDowMC4wMDBaIiwiaWQiOiJjLTE3NjAyODAwMDAwMDAtazNqOSJ9" } ``` | Parameter or field | Meaning | | - | - | | `limit` | How many items per page, 1 to 100. Default 25. | | `starting_after` | The `next_cursor` from the previous page. Leave it out for the first page. | | `has_more` | `true` when there is another page | | `next_cursor` | Pass it as `starting_after` to get the next page; `null` on the last page | Treat the cursor as opaque: pass it back exactly as you received it. A cursor that was edited or came from another list returns `400` [`invalid_request`](/api-reference/errors#invalid_request) with `param: "starting_after"`. ## Order Lists are newest first. Conversations are ordered by when they started, or by when they last changed when you pass `updated_since`. Contacts and meetings are ordered by their last change. Events are ordered by when they happened. ## Read every page ```javascript Node theme={"theme":"vesper"} async function* all(path, params = {}) { let cursor = null; do { const url = new URL(`https://api.meetdarwin.ai/v1${path}`); for (const [k, v] of Object.entries({ limit: 100, ...params })) url.searchParams.set(k, v); if (cursor) url.searchParams.set("starting_after", cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.DARWIN_API_KEY}` } }); if (!res.ok) throw new Error(`${res.status} ${(await res.json()).code}`); const page = await res.json(); yield* page.data; cursor = page.has_more ? page.next_cursor : null; } while (cursor); } for await (const meeting of all("/meetings", { verified: "true" })) console.log(meeting.id, meeting.start_at); ``` ```python Python theme={"theme":"vesper"} import os, requests def all_items(path, **params): params = {"limit": 100, **params} while True: res = requests.get( f"https://api.meetdarwin.ai/v1{path}", params=params, headers={"Authorization": f"Bearer {os.environ['DARWIN_API_KEY']}"}, ) res.raise_for_status() page = res.json() yield from page["data"] if not page["has_more"]: return params["starting_after"] = page["next_cursor"] for meeting in all_items("/meetings", verified="true"): print(meeting["id"], meeting["start_at"]) ``` ## Sync only what changed A sync job should not read everything every time. Pass the time of your last run: | List | Filter | | - | - | | `GET /conversations` | `updated_since` (a new message, a lead or a booking) or `created_since`. Darwin's grade arrives separately: use the `conversation.completed` webhook or `graded=true`. | | `GET /contacts` | `updated_since` | | `GET /meetings` | `updated_since` (booked, rescheduled or canceled) | | `GET /events` | `created_since` (the last 30 days) | Values are ISO 8601, for example `2026-10-01T00:00:00Z`. A value that isn't a date returns `400` with `param` naming the filter. A safe loop: 1. Note the current time **before** you start reading. Call it `started`. 2. Read every page with `updated_since` set to the `started` time of your last successful run. 3. Upsert each item by its `id`. The same item can appear in two runs; that's expected. 4. Only when every page succeeded, save `started` for the next run. Something that changes while you are paging moves to the front of the list and may be skipped in this run, but its new `updated_since` time is after `started`, so the next run picks it up. See the [CRM sync recipe](/guides/crm-sync) for a full example. ## Lists without paging `GET /webhook_endpoints` returns all of a workspace's endpoints (up to 16) in one page. `GET /chat_buttons` likewise returns every chat button. # Change a greeting Source: https://docs.meetdarwin.ai/api-reference/personal-links/change-a-greeting /api-reference/openapi.json patch /link_greetings/{id} Requires the `agent:write` scope. Reaches every link that uses it, including emails already sent. # Change a personal link Source: https://docs.meetdarwin.ai/api-reference/personal-links/change-a-personal-link /api-reference/openapi.json patch /personal_links/{id} Requires the `agent:write` scope. A new greeting reaches every email already sent, because the greeting is looked up by the link's id. A new page only applies to addresses made after the change. # Create a greeting Source: https://docs.meetdarwin.ai/api-reference/personal-links/create-a-greeting /api-reference/openapi.json post /link_greetings Requires the `agent:write` scope. # Create a personal link Source: https://docs.meetdarwin.ai/api-reference/personal-links/create-a-personal-link /api-reference/openapi.json post /personal_links Requires the `agent:write` scope. Name a greeting (gr_…) or write a new_greeting in the same call. Darwin makes the link's id. # Delete a greeting Source: https://docs.meetdarwin.ai/api-reference/personal-links/delete-a-greeting /api-reference/openapi.json delete /link_greetings/{id} Requires the `agent:write` scope. Links that used it become drafts, which greet no one, until you give them another greeting. # Delete a personal link Source: https://docs.meetdarwin.ai/api-reference/personal-links/delete-a-personal-link /api-reference/openapi.json delete /personal_links/{id} Requires the `agent:write` scope. Someone who opens it later is greeted with your first greeting. # List greetings for personal links Source: https://docs.meetdarwin.ai/api-reference/personal-links/list-greetings-for-personal-links /api-reference/openapi.json get /link_greetings Requires the `agent:read` scope. # List personal links Source: https://docs.meetdarwin.ai/api-reference/personal-links/list-personal-links /api-reference/openapi.json get /personal_links Requires the `agent:read` scope. Each link with its greeting, its address with every email tool's merge fields, and its results. # List visits from personal links Source: https://docs.meetdarwin.ai/api-reference/personal-links/list-visits-from-personal-links /api-reference/openapi.json get /link_visits Requires the `analytics:read` scope. Names, emails, companies and titles need contacts:read. # Make each person's own address for a link Source: https://docs.meetdarwin.ai/api-reference/personal-links/make-each-persons-own-address-for-a-link /api-reference/openapi.json post /personal_links/{id}/urls Requires the `agent:read` scope. Up to 1,000 people per call, each with any of email, first_name, last_name, company and title, returned in order with their address and the greeting they will see. Nothing is stored: a person becomes a contact when they click. # Retrieve a greeting Source: https://docs.meetdarwin.ai/api-reference/personal-links/retrieve-a-greeting /api-reference/openapi.json get /link_greetings/{id} Requires the `agent:read` scope. # Retrieve a personal link Source: https://docs.meetdarwin.ai/api-reference/personal-links/retrieve-a-personal-link /api-reference/openapi.json get /personal_links/{id} Requires the `agent:read` scope. # Retrieve a visit from a personal link Source: https://docs.meetdarwin.ai/api-reference/personal-links/retrieve-a-visit-from-a-personal-link /api-reference/openapi.json get /link_visits/{id} Requires the `analytics:read` scope. # Erase or export a person's data Source: https://docs.meetdarwin.ai/api-reference/privacy Answer data subject requests with the Darwin API: export everything held about one person by email, or erase their contact, transcripts and booking details for good. What is removed and what is kept. When someone asks what you hold about them, or asks you to delete it, the Darwin API answers for everything Darwin keeps about them in one call. Both calls take the person's email address and need a key with the `privacy:write` scope. Create a key just for this and keep it with whoever handles privacy requests. Both are `POST` requests, so the email address never appears in a URL or a server log. ## Export what Darwin holds ```bash theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/privacy/exports \ -H "Authorization: Bearer $PRIVACY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email": "jordan@example.com"}' ``` The answer is one JSON document: | Field | What it holds | | - | - | | `object` | `privacy_export` | | `email` | The address, lowercased | | `generated_at` | When the export was made | | `contact` | Their contact record, or `null` | | `conversations` | Every conversation that names them, with the page, times, the name and email they gave, booking and handoff details, and the full transcript | | `booking_attempts` | Native booking attempts linked to them or their conversations | | `link_bookings` | Bookings made through a booking link, with the provider's details | Hand it to the person as it is, or turn it into your own format. An export is a heavy request for [rate limits](/api-reference/rate-limits). ## Erase a person > [!WARNING] > > Erasure is permanent. Darwin cannot restore what it removes, and neither can Darwin support. Export first if you need a copy. ```bash theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/privacy/erasures \ -H "Authorization: Bearer $PRIVACY_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: erase-jordan-2026-10-12" \ -d '{"email": "jordan@example.com"}' ``` If you already have their contact id, `DELETE /v1/contacts/{id}` does the same and returns the same receipt inside `erasure`. The `201` response is a receipt: ```json theme={"theme":"vesper"} { "object": "privacy_erasure", "id": "era_k2m9q1x7z3c5v8b4", "email_hash": "9a3f…", "conversations": 3, "booking_attempts": 1, "link_bookings": 0, "events": 4, "contact_deleted": true } ``` The counts say how many records were changed. Darwin keeps this receipt with a hash of the address, never the address itself, so you can show an erasure took place without keeping what was erased. **It worked if** `GET /v1/contacts?email=jordan@example.com` returns an empty list and the conversations counted in the receipt have no transcript. ### What erasure removes In one step, everywhere Darwin keeps it: * **The contact** is deleted. * **Every conversation that names them** loses its transcript, the lead's name and email, the booker's email and the handoff note. Darwin's grading notes on those conversations go too. * **Booking attempts** lose their details. **Link bookings** lose the name, email and the provider's details. * **Chat API sessions** lose the identity and external id you passed. * **Events** about them keep their type and time and lose their payload. Webhooks for those events that have not gone out yet send `"data": { "erased": true }`. ### What erasure keeps * Each conversation's row and timestamps, so last month's counts, opening-test results and billing don't change. * The receipt described above. ### What it can't reach * **Emails already sent** to your team about a booking or a lead are in their inboxes. * **Your calendar provider's copy** of the meeting. Remove it in Calendly or Google Calendar. * **Systems you sent data to**: your CRM, Slack, warehouse or anything else that received webhooks or API reads. Erase it there too. ## Running both from code ```javascript Node theme={"theme":"vesper"} async function privacyRequest(kind, email) { // kind: "exports" or "erasures" const res = await fetch(`https://api.meetdarwin.ai/v1/privacy/${kind}`, { method: "POST", headers: { Authorization: `Bearer ${process.env.PRIVACY_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ email }), }); if (!res.ok) throw new Error((await res.json()).detail); return res.json(); } const copy = await privacyRequest("exports", "jordan@example.com"); const receipt = await privacyRequest("erasures", "jordan@example.com"); ``` ```python Python theme={"theme":"vesper"} import os, requests def privacy_request(kind, email): # kind: "exports" or "erasures" res = requests.post( f"https://api.meetdarwin.ai/v1/privacy/{kind}", json={"email": email}, headers={"Authorization": f"Bearer {os.environ['PRIVACY_API_KEY']}"}, ) res.raise_for_status() return res.json() copy = privacy_request("exports", "jordan@example.com") receipt = privacy_request("erasures", "jordan@example.com") ``` An address that isn't a usable email returns `400` [`invalid_request`](/api-reference/errors#invalid_request) with `param: "email"`. An address Darwin holds nothing about still succeeds, with zero counts and `contact_deleted: false`. # Erase everything held about a person Source: https://docs.meetdarwin.ai/api-reference/privacy/erase-everything-held-about-a-person /api-reference/openapi.json post /privacy/erasures Requires the `privacy:write` scope. # Export everything held about a person Source: https://docs.meetdarwin.ai/api-reference/privacy/export-everything-held-about-a-person /api-reference/openapi.json post /privacy/exports Requires the `privacy:write` scope. # API rate limits Source: https://docs.meetdarwin.ai/api-reference/rate-limits Darwin API rate limits per key per minute, the same on every plan (legacy plans keep theirs), the smaller budget for heavy calls, RateLimit headers and how to handle 429. The API is on every Darwin plan, with the same limits on each: **600 requests a minute** and **30 heavy requests a minute**, per API key. Limits apply per key, so two integrations with their own keys don't slow each other down. Legacy plans keep their limits: Origin and Adapt 120 (10), Evolve 600 (30) and Apex 1,200 (60). Heavy requests run a model, read a website or build an export. They have their own budget and do not use up the standard one: * `POST /chat/sessions/{id}/messages` (each chat API message) * `POST /knowledge/documents` and `PATCH /knowledge/documents/{id}` * `POST /knowledge/sources/{id}/refresh` * `POST /privacy/exports` * `POST /webhook_endpoints/{id}/test` Every other endpoint counts against the standard budget. Each [MCP](/api-reference/mcp) tool call counts as one standard request for the connection's key. ## Headers Every successful response carries the budget it was counted against: | Header | Meaning | | - | - | | `RateLimit-Limit` | Requests allowed in the current one-minute window | | `RateLimit-Remaining` | Requests left in this window | | `RateLimit-Reset` | Seconds until the window resets | The window is a fixed minute that starts with the key's first request. ## When you hit the limit The request is refused with `429` and the code [`rate_limited`](/api-reference/errors#rate_limited). The response carries `Retry-After` with the seconds to wait, along with the `RateLimit-*` headers. Nothing was done, so retrying after the wait is safe. ```http theme={"theme":"vesper"} HTTP/1.1 429 Too Many Requests Content-Type: application/problem+json Retry-After: 23 RateLimit-Limit: 600 RateLimit-Remaining: 0 RateLimit-Reset: 23 ``` To stay under the limit: * Use [webhooks](/api-reference/webhooks) instead of polling for new leads and meetings. * Read with `limit=100` and `updated_since` rather than many small pages. See [Pagination](/api-reference/pagination). * Give each integration its own key. * Spread a large backfill over several minutes. The [chat API](/api-reference/chat-api#monthly-allowance) also has a monthly allowance of conversations, separate from these per-minute limits. # Guides Source: https://docs.meetdarwin.ai/api-reference/recipes Working examples for the Darwin API: post handoff requests to Slack with a webhook, sync new leads to a CRM with updated_since, export conversations to a data warehouse every night, give every prospect their own personal link, tell the rep when a prospect opens it, build a quote form from code, and run opening tests with your own AI assistant. Complete examples of common jobs, each small enough to read in one sitting. They use Node 18 or later and plain `fetch`; swap in your own CRM, warehouse or hosting, or use the [TypeScript SDK](/sdks/typescript) for typed calls, retries and pagination. > [card] [Send handoffs to Slack](/guides/slack-handoffs) > > A webhook receiver that posts requests for a person to a channel. > [card] [Sync leads to a CRM](/guides/crm-sync) > > Copy new contacts to your CRM every 15 minutes with updated\_since. > [card] [Export conversations to your warehouse nightly](/guides/warehouse-export) > > Conversations, transcripts and grades into BigQuery, Snowflake or files. > [card] [Give every prospect their own personal link](/guides/personal-links) > > Each prospect's own link on their CRM record, made in batches of 1,000. > [card] [Tell the rep in Slack when a prospect opens their link](/guides/link-opened-alerts) > > A Slack message the moment a prospect opens their link. > [card] [Create a quote form with the API](/guides/quote-form) > > Build an outcome form from code and switch it live. > [card] [Run opening tests with your own AI assistant](/guides/opening-tests) > > Let Claude, ChatGPT or your code run the tests while Darwin scores them. # Webhooks Source: https://docs.meetdarwin.ai/api-reference/webhooks Get Darwin events (lead captured, handoff requested, form request sent, meeting booked, conversation graded) posted to your URL, signed with Standard Webhooks. Events, payloads, verification, retries and redelivery. **Find it in Darwin** Settings → Integrations → API → Webhooks [Open this screen ↗](https://app.meetdarwin.ai/admin/settings/integrations/api) Webhooks tell your systems the moment something happens in the chat: a visitor leaves their email, asks for a person, or books a meeting. Darwin sends each event as a signed `POST` to your HTTPS address and retries for about three days if your server doesn't answer. Every event is something Darwin's server recorded, never something a browser claimed. Your own preview chats send no events; use **Send test event** to try an endpoint. ## Add an endpoint > [tab] In Darwin > > **1. Open Webhooks** > > Open **Settings → Integrations → API**. Owners and admins see **Add endpoint** beside **Webhooks**. > > **2. Enter your URL** > > Select **Add endpoint** and enter an HTTPS **URL** on a public hostname, for example `https://example.com/webhooks/darwin`. It must answer `2xx` within 10 seconds. > > **3. Choose events** > > Under **Events**, keep **Choose events** and tick what you need. `lead.captured`, `handoff.requested` and `meeting.booked` are ticked for you. **All events** subscribes to every type, including ones Darwin adds later. > > **4. Copy the signing secret** > > Select **Add endpoint**. Darwin shows the endpoint's **Signing secret** (`whsec_…`). Store it with your receiver's other secrets. Owners and admins can see it again later with **Show signing secret**. > > **5. Send a test event** > > Open the endpoint's **⋯** menu and select **Send test event**. **It worked if** Darwin says **The endpoint answered 200** (or another `2xx`). Otherwise it shows why, for example `HTTP 401` or `No answer within 10 seconds.` > [tab] With the API > > Needs a key with `webhooks:write`. > > ```bash theme={"theme":"vesper"} > curl https://api.meetdarwin.ai/v1/webhook_endpoints \ > -H "Authorization: Bearer $DARWIN_API_KEY" \ > -H "Content-Type: application/json" \ > -H "Idempotency-Key: $(uuidgen)" \ > -d '{ > "url": "https://example.com/webhooks/darwin", > "events": ["lead.captured", "handoff.requested", "meeting.booked"], > "description": "CRM sync" > }' > ``` > > The `201` response is the endpoint, and only this response includes its `secret`: > > ```json theme={"theme":"vesper"} > { > "id": "we_7h3k9m2p4q8r1s5t", > "object": "webhook_endpoint", > "url": "https://example.com/webhooks/darwin", > "description": "CRM sync", > "events": ["handoff.requested", "lead.captured", "meeting.booked"], > "status": "enabled", > "disabled_reason": null, > "failing_since": null, > "created_at": "2026-10-12T09:30:00.000Z", > "updated_at": "2026-10-12T09:30:00.000Z", > "secret": "whsec_..." > } > ``` > > Use `"events": ["*"]` for every type. Then send a test: `POST /v1/webhook_endpoints/we_…/test`. The response says whether your endpoint answered, with its `status`, `duration_ms` and the start of its `response`. A workspace can have 16 endpoints. A new endpoint receives events that happen after it was created; for older ones, read [`GET /events`](/api-reference/webhooks/delivery#catch-up-with-the-event-log). ## What Darwin sends ```http theme={"theme":"vesper"} POST /webhooks/darwin HTTP/1.1 Content-Type: application/json User-Agent: Darwin-Webhooks/1.0 (+https://docs.meetdarwin.ai/api-reference/webhooks) webhook-id: evt_2b8f0c1d6e4a9937 webhook-timestamp: 1760261400 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= ``` The body is one event: | Field | Meaning | | - | - | | `id` | The event id. Also sent as `webhook-id`. | | `object` | Always `event` | | `type` | One of the types above | | `created_at` | When it happened | | `api_version` | The [API version](/api-reference/introduction#versioning) the payload follows | | `data` | `object`, and for some types `conversation` or `error` | Darwin builds the payload a few seconds after the event and keeps it: a retry or redelivery sends the same body. For the latest state, read the object from the API by its id. Every event type and an example payload for each are in [Events and payloads](/api-reference/webhooks/events). ## Requirements for your URL * **HTTPS** on a **public hostname**. Plain HTTP, IP addresses, `localhost` and `.local` or `.internal` names are refused, and so is a hostname that resolves to a private network address. * No username or password before the host. To authenticate requests, verify the signature, or put a token in the path or query string. * Darwin does not publish a fixed list of sending IP addresses. Verify signatures rather than allowlisting IPs. ## Change or remove an endpoint * Change its URL, events or description with `PATCH /v1/webhook_endpoints/{id}`. * **Delete** in the **⋯** menu (or `DELETE /v1/webhook_endpoints/{id}`) stops all deliveries and removes the endpoint's delivery log. ## Next > [card] [Events and payloads](/api-reference/webhooks/events) > > Every event type, when it fires and what it carries. > [card] [Verify signatures](/api-reference/webhooks/verify) > > Check every delivery came from Darwin, with or without a library. > [card] [Delivery and retries](/api-reference/webhooks/delivery) > > Timeouts, duplicates, retries, redelivery and the event log. > [card] [Send handoffs to Slack](/guides/slack-handoffs) > > A complete receiver in one file. # Create a webhook endpoint Source: https://docs.meetdarwin.ai/api-reference/webhooks/create-a-webhook-endpoint /api-reference/openapi.json post /webhook_endpoints Requires the `webhooks:write` scope. # Delete a webhook endpoint Source: https://docs.meetdarwin.ai/api-reference/webhooks/delete-a-webhook-endpoint /api-reference/openapi.json delete /webhook_endpoints/{id} Requires the `webhooks:write` scope. # Webhook delivery and retries Source: https://docs.meetdarwin.ai/api-reference/webhooks/delivery How Darwin delivers webhooks: answer 2xx within 10 seconds, handle duplicates and out-of-order events, the retry schedule over three days, switched-off endpoints, redelivery and catching up from the event log. Darwin delivers each event at least once, retries for about three days and keeps every event for 30 days. This page covers what your receiver has to do and how to recover when it was down. ## Answer quickly * Answer with any `2xx` status within **10 seconds**. Anything else counts as a failure: a `3xx` (Darwin does not follow redirects), a `4xx`, a `5xx`, a timeout or a refused connection. * Answer first and do slow work (CRM calls, Slack posts) afterwards in a queue. A receiver that waits on another service is the most common cause of timeouts. * Darwin reads at most the first 64 KB of your answer and keeps the first 500 characters in the delivery log. Don't return anything secret. ## Handle duplicates and order * **At least once.** Darwin may deliver the same event more than once, for example when your server handled it but the answer was lost. `webhook-id` (the event `id`) is the same on every retry and redelivery. Record the ids you've processed and skip repeats. * **No ordering.** Events can arrive out of order, especially after retries. A `meeting.rescheduled` can arrive before its `meeting.booked`. Use `created_at`, or the object's `updated_at`, to decide which is newer, or read the current object from the API. ## Retries If a delivery fails, Darwin tries again after: | Retry | Wait after the previous attempt | | - | - | | 1 | 1 minute | | 2 | 5 minutes | | 3 | 30 minutes | | 4 | 2 hours | | 5 | 6 hours | | 6 | 12 hours | | 7 | 24 hours | | 8 | 24 hours | That is nine attempts over about three days. After the last one the delivery is marked **Failed**. You can still send it again by hand for 30 days. ### Endpoints that keep failing are switched off If an endpoint has failures and not one successful delivery for **three days**, and is still failing, Darwin switches it off and shows why under its address in **Settings → Integrations → API**: *Deliveries have failed for three days without one success. Fix the receiver, then switch the endpoint back on.* An endpoint that answers `410 Gone` is switched off at once: that is how a receiver (Zapier, for one) says the subscription is over. Its badge turns from **Failing** to **Off**, and `GET /webhook_endpoints/{id}` shows `"status": "disabled"` with a `disabled_reason`. While an endpoint is off, Darwin does not queue new events for it. To recover: 1. Fix your receiver and check it with **Send test event**. 2. Open the endpoint's **⋯** menu and select **Switch on** (or `PATCH` it with `{"status": "enabled"}`). Deliveries that were still retrying continue. 3. Send the events you missed: use **Send again** in **Deliveries**, or read [`GET /events`](/api-reference/webhooks/delivery#catch-up-with-the-event-log) for the gap and redeliver them. You can also select **Switch off** yourself, for example during maintenance. Events stay in the event log for 30 days. ## Check deliveries and send again Open the endpoint's **⋯** menu and select **Deliveries**. Each row shows the event type, whether it was **Delivered**, is **Retrying** or **Failed**, the number of attempts, your server's last HTTP status or error, and the next try. **Send again** puts that delivery back in the queue; it goes out within a few seconds. The endpoint row also summarises this week: how many were delivered, failed and are retrying. With the API: ```bash theme={"theme":"vesper"} # Failed deliveries for one endpoint curl "https://api.meetdarwin.ai/v1/webhook_endpoints/we_7h3k9m2p4q8r1s5t/deliveries?status=failed" \ -H "Authorization: Bearer $DARWIN_API_KEY" # Send one event again to every enabled endpoint subscribed to its type... curl -X POST https://api.meetdarwin.ai/v1/events/evt_2b8f0c1d6e4a9937/redeliver \ -H "Authorization: Bearer $DARWIN_API_KEY" # ...or to one endpoint curl -X POST https://api.meetdarwin.ai/v1/events/evt_2b8f0c1d6e4a9937/redeliver \ -H "Authorization: Bearer $DARWIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"endpoint": "we_7h3k9m2p4q8r1s5t"}' ``` The redeliver response says how many deliveries were `queued`. Only enabled endpoints subscribed to the event's type receive it. ## Catch up with the event log Every event from the last 30 days is in `GET /events` (scope `events:read`), newest first. It is the pull twin of webhooks: use it after downtime, or instead of webhooks if you would rather poll. ```bash theme={"theme":"vesper"} curl "https://api.meetdarwin.ai/v1/events?type=lead.captured,meeting.booked&created_since=2026-10-10T00:00:00Z&limit=100" \ -H "Authorization: Bearer $DARWIN_API_KEY" ``` Each item is the same event body a webhook sends. ## Troubleshooting > [accordion] Every delivery fails with a signature error on my side > > Check that you verify the raw body, not parsed and re-serialised JSON, and that you base64-decode the secret after removing `whsec_`. Frameworks that parse JSON automatically need a raw-body route, like `express.raw` in [Verify signatures](/api-reference/webhooks/verify). > [accordion] Redirects are not followed > > Your URL answered with a `3xx`. Use the final address, including `https://` and the exact path, often with or without a trailing slash. > [accordion] No answer within 10 seconds > > Answer `200` before doing the work. Check that your server is reachable from the public internet and not behind a login or firewall. > [accordion] I don't get events for my test chats > > Preview chats in Darwin never send events. Use **Send test event**, or have someone use the chat on your live website. [Send handoffs to Slack →](/guides/slack-handoffs) · [Sync leads to a CRM →](/guides/crm-sync) # Webhook events and payloads Source: https://docs.meetdarwin.ai/api-reference/webhooks/events Every Darwin webhook event type: lead.captured, handoff.requested, outcome.submitted, personal_link.opened, meeting.booked and the rest, when each fires and an example payload for each. Each event is one JSON body with the envelope described in [What Darwin sends](/api-reference/webhooks#what-darwin-sends). `data.object` is the thing the event is about. ## Events | Type | Sent when | `data.object` is | | - | - | - | | `lead.captured` | A visitor gave their name and email in the chat | The contact, plus `data.conversation` | | `handoff.requested` | A visitor asked to talk to a person (once per conversation) | The conversation, with its transcript | | `outcome.submitted` | A visitor sent one of your [outcome forms](https://www.meetdarwin.ai/help/outcomes/what-are-outcomes), such as a quote request, with every answer | The outcome request, plus `data.conversation` | | `personal_link.opened` | Someone opened one of your [personal links](https://www.meetdarwin.ai/help/links/personal-links), whether or not they chat. Once per browser and link within 12 hours; your team's and test clicks never send it. | The link visit, plus `data.link` and `data.contact` | | `meeting.booked` | A meeting was booked from the chat | The meeting | | `meeting.rescheduled` | A meeting booked from the chat moved to a new time | The meeting | | `meeting.canceled` | A meeting booked from the chat was canceled | The meeting | | `conversation.started` | A visitor sent their first message. High volume: subscribe only if you mirror every chat. | The conversation | | `conversation.completed` | A conversation went quiet and Darwin graded it: intent, buying stage and score | The conversation, with its transcript and `evaluation` | | `experiment.started` | An opening test started showing variants to visitors | The experiment | | `experiment.winner_found` | An opening test found a winner | The experiment | | `knowledge.source_failed` | A page the agent answers from could not be read | The knowledge page, plus `data.error` | `data.object` has the same shape as the REST API's answer for that object, so code that reads `GET /conversations/{id}` also reads the webhook. Webhooks always include names and emails: an endpoint exists to get the lead into your CRM. ## Example payloads > [accordion] lead.captured > > ```json theme={"theme":"vesper"} > { > "id": "evt_2b8f0c1d6e4a9937", > "object": "event", > "type": "lead.captured", > "created_at": "2026-10-12T09:30:00.000Z", > "api_version": "2026-10-12", > "data": { > "object": { > "id": "ct_4f8k2m9q1x7z3c5v", > "object": "contact", > "email": "jordan@example.com", > "first_name": "Jordan", > "last_name": "Lee", > "name": "Jordan Lee", > "company": null, > "phone": null, > "external_id": null, > "attributes": {}, > "identity_verified": false, > "source": "chat", > "first_seen_at": "2026-10-12T09:27:41.000Z", > "last_seen_at": "2026-10-12T09:30:00.000Z", > "created_at": "2026-10-12T09:30:00.000Z", > "updated_at": "2026-10-12T09:30:00.000Z" > }, > "conversation": { > "id": "conv_c-1760261261000-k3j9", > "object": "conversation", > "page_url": "https://example.com/pricing", > "page_path": "/pricing", > "outcome": "lead", > "message_count": 6, > "preview": "Do you offer annual billing for teams of 20?", > "contact": "ct_4f8k2m9q1x7z3c5v", > "lead": { "first_name": "Jordan", "last_name": "Lee", "email": "jordan@example.com", "captured_at": "2026-10-12T09:30:00.000Z" }, > "visitor": { "device": "desktop", "country": "US", "source": "google", "utm": {}, "ai_referred": false }, > "test": false > } > } > } > ``` > > The conversation here is shortened; it carries every field of a conversation, without the transcript. > [accordion] handoff.requested and conversation.completed > > ```json theme={"theme":"vesper"} > { > "id": "evt_9c0d2e7f1a3b5c6d", > "object": "event", > "type": "handoff.requested", > "created_at": "2026-10-12T10:02:13.000Z", > "api_version": "2026-10-12", > "data": { > "object": { > "id": "conv_c-1760262900000-p2r8", > "object": "conversation", > "page_url": "https://example.com/support", > "outcome": "handoff", > "handoff": { "requested_at": "2026-10-12T10:02:13.000Z", "reason": "Wants to change the billing contact on their account" }, > "lead": { "first_name": "Sam", "last_name": null, "email": "sam@example.com", "captured_at": "2026-10-12T10:01:50.000Z" }, > "evaluation": null, > "messages": [ > { "id": "m1", "role": "user", "text": "Can I talk to someone about our invoice?", "created_at": "2026-10-12T10:01:02.000Z", "tools": [] }, > { "id": "m2", "role": "assistant", "text": "I've passed this to the team. They'll email you at sam@example.com.", "created_at": "2026-10-12T10:02:13.000Z", "tools": ["asked for a person"] } > ] > } > } > } > ``` > > `conversation.completed` has the same shape. Its `evaluation` is filled in: `intent`, `buying_stage`, `qualification_score` and `meeting_intent` (0 to 10), `understood`, `failure_category`, `abandonment_reason` and `recommended_improvement`. `conversation.started` carries the conversation without `messages`. > [accordion] outcome.submitted > > ```json theme={"theme":"vesper"} > { > "id": "evt_9c8d7e6f5a4b3c2d", > "object": "event", > "type": "outcome.submitted", > "created_at": "2026-10-12T10:40:00.000Z", > "api_version": "2026-10-12", > "data": { > "object": { > "id": "oreq_6f1c2a9e0b3d4e5f8a7b6c5d4e3f2a1b", > "object": "outcome_request", > "outcome": { "id": "out_3k9d2m7q1x8w", "name": "Request a quote" }, > "status": "sent", > "fit": true, > "fit_reasons": [], > "answers": [ > { "key": "what_do_you_need_a_quote_for", "question": "What do you need a quote for?", "value": "Brand refresh" }, > { "key": "when_do_you_want_it_done", "question": "When do you want it done?", "value": "Within a month" } > ], > "fields": { > "what_do_you_need_a_quote_for": "Brand refresh", > "when_do_you_want_it_done": "Within a month" > }, > "contact": { "name": "Priya Shah", "email": "priya@example.com", "phone": null }, > "contact_id": "ct_7q2w9e4r1t6y3u8i", > "conversation": "conv_c-1760265600000-k3p9", > "page_url": "https://example.com/services", > "billing": "counted", > "submitted_at": "2026-10-12T10:40:00.000Z", > "created_at": "2026-10-12T10:38:12.000Z", > "test": false > }, > "conversation": { > "id": "conv_c-1760265600000-k3p9", > "object": "conversation", > "page_url": "https://example.com/services", > "page_path": "/services", > "outcome": "lead", > "message_count": 4, > "preview": "How much would a brand refresh cost?", > "contact": "ct_7q2w9e4r1t6y3u8i", > "test": false > } > } > } > ``` > > The conversation here is shortened; it carries every field of a conversation, with its transcript in `messages`. Each answer's `key` comes from its question and stays the same while the question does; `GET /outcomes` lists every form's keys. `fit` is `false` when an answer matched the form's fit rules, and `fit_reasons` names those questions. `billing` is `counted`, `not_billable`, `voided` or `none`. > [accordion] personal\_link.opened > > ```json theme={"theme":"vesper"} > { > "id": "evt_4a8c2e6b0d1f5539", > "object": "event", > "type": "personal_link.opened", > "created_at": "2026-10-12T14:01:31.000Z", > "api_version": "2026-10-12", > "data": { > "object": { > "id": "lv_3f9a0c1d2b4e5f60718293a4b5c6d7e8", > "object": "link_visit", > "link": { "id": "lk_q4x7mz", "name": "Q4 outbound" }, > "greeting": "gr_m3k8wq", > "page": "/pricing", > "person": { "email": "jane@example.com", "first_name": "Jane", "last_name": "Cooper", "company": "Acme", "title": "VP of Operations" }, > "contact": "ct_7q2w9e4r1t6y3u8i", > "conversations": [], > "created_at": "2026-10-12T14:01:30.000Z", > "last_seen_at": "2026-10-12T14:01:30.000Z", > "test": false > }, > "link": { > "id": "lk_q4x7mz", > "object": "personal_link", > "name": "Q4 outbound", > "page": "/pricing", > "greeting": "gr_m3k8wq", > "status": "active", > "url": "https://example.com/pricing?dw=lk_q4x7mz", > "results": { "days": 30, "visits": 42, "chats": 11, "meetings": 3 } > }, > "contact": { > "id": "ct_7q2w9e4r1t6y3u8i", > "object": "contact", > "email": "jane@example.com", > "first_name": "Jane", > "last_name": "Cooper", > "company": "Acme", > "source": "link" > } > } > } > ``` > > The link and contact here are shortened; they carry every field `GET /personal_links/{id}` and `GET /contacts/{id}` return. `person` is what the link's address carried, so a forwarded email can carry someone else's details. `contact` is null when the link had no email, and `greeting` is null when the link has no greeting: the page opened with its own opening, and the agent still knows who arrived. The visit arrives within seconds of the click, before any chat; `GET /link_visits/{id}` later lists the `conversations` that browser started. > [accordion] meeting.booked, meeting.rescheduled and meeting.canceled > > ```json theme={"theme":"vesper"} > { > "id": "evt_5e6f7a8b9c0d1e2f", > "object": "event", > "type": "meeting.booked", > "created_at": "2026-10-12T11:15:00.000Z", > "api_version": "2026-10-12", > "data": { > "object": { > "id": "mtg_n4f8a2b1c9d0e7f6a5b4c3d2e1f0a9b8c", > "object": "meeting", > "status": "booked", > "verified": true, > "provider": "calendly", > "meeting_type": "30 minute demo", > "host": "Alex Rivera", > "start_at": "2026-10-14T16:00:00.000Z", > "end_at": "2026-10-14T16:30:00.000Z", > "booked_at": "2026-10-12T11:15:00.000Z", > "reschedules": 0, > "join_url": "https://zoom.us/j/0000000000", > "reschedule_url": "https://calendly.com/reschedulings/...", > "cancel_url": "https://calendly.com/cancellations/...", > "conversation": "conv_c-1760267400000-x8m2", > "contact": "ct_7q2w9e4r1t6y3u8i", > "invitee": { "name": "Priya Shah", "email": "priya@example.com" }, > "updated_at": "2026-10-12T11:15:00.000Z", > "test": false > } > } > } > ``` > > `verified` is `true` when the calendar provider confirmed the booking. On `meeting.rescheduled`, `start_at`, `end_at` and `reschedules` change; on `meeting.canceled`, `status` is `canceled`. > [accordion] experiment.started and experiment.winner\_found > > ```json theme={"theme":"vesper"} > { > "id": "evt_1a2b3c4d5e6f7a8b", > "object": "event", > "type": "experiment.winner_found", > "created_at": "2026-10-12T12:00:00.000Z", > "api_version": "2026-10-12", > "data": { > "object": { > "id": "exp_4c1f0a2b9d3e7a10", > "object": "experiment", > "name": "Pricing page opening", > "status": "winner_found", > "pages": ["/pricing"], > "metric": "bookings", > "winner": "var_...", > "variants": [ > { "id": "var_...", "object": "variant", "name": "Current", "control": true, "headline": "Questions about pricing?", "results": { "views": 1840, "rate": 0.021, "chance_beats_current": null } } > ] > } > } > } > ``` > > Shortened. The experiment has the shape of `GET /experiments/{id}`. > [accordion] knowledge.source\_failed > > ```json theme={"theme":"vesper"} > { > "id": "evt_8b7a6f5e4d3c2b1a", > "object": "event", > "type": "knowledge.source_failed", > "created_at": "2026-10-12T03:10:00.000Z", > "api_version": "2026-10-12", > "data": { > "object": { > "id": "src_142", > "object": "knowledge_source", > "url": "https://example.com/docs/security", > "section": "/docs/", > "title": "Security", > "status": "failed", > "error": "The page answered 404.", > "characters": 5120, > "chunks": 4, > "read_at": "2026-10-05T03:10:00.000Z", > "created_at": "2026-09-20T14:00:00.000Z" > }, > "error": "The page answered 404." > } > } > ``` > > A page that fails keeps its last good content, so the agent keeps answering from it until you fix or remove it. > [accordion] webhook.test > > ```json theme={"theme":"vesper"} > { > "id": "evt_0f1e2d3c4b5a6978", > "object": "event", > "type": "webhook.test", > "created_at": "2026-10-12T09:31:00.000Z", > "api_version": "2026-10-12", > "data": { > "object": { > "endpoint": "we_7h3k9m2p4q8r1s5t", > "message": "This is a test event from Darwin. Your endpoint works if it answered 2xx." > } > } > } > ``` > > Sent only by **Send test event**. It is signed like a real event, sent once and not retried, and does not appear in `GET /events`. If the object no longer exists when Darwin builds the payload, `data.object` is `null`. If the person was [erased](/api-reference/privacy), `data` is `{ "erased": true }`. # List webhook endpoints Source: https://docs.meetdarwin.ai/api-reference/webhooks/list-webhook-endpoints /api-reference/openapi.json get /webhook_endpoints Requires the `webhooks:write` scope. # Retrieve a webhook endpoint Source: https://docs.meetdarwin.ai/api-reference/webhooks/retrieve-a-webhook-endpoint /api-reference/openapi.json get /webhook_endpoints/{id} Requires the `webhooks:write` scope. # Rotate the signing secret Source: https://docs.meetdarwin.ai/api-reference/webhooks/rotate-the-signing-secret /api-reference/openapi.json post /webhook_endpoints/{id}/rotate_secret Requires the `webhooks:write` scope. # Send a test event now Source: https://docs.meetdarwin.ai/api-reference/webhooks/send-a-test-event-now /api-reference/openapi.json post /webhook_endpoints/{id}/test Requires the `webhooks:write` scope. # Send an event to webhooks again Source: https://docs.meetdarwin.ai/api-reference/webhooks/send-an-event-to-webhooks-again /api-reference/openapi.json post /events/{id}/redeliver Requires the `webhooks:write` scope. # The endpoint's delivery log Source: https://docs.meetdarwin.ai/api-reference/webhooks/the-endpoints-delivery-log /api-reference/openapi.json get /webhook_endpoints/{id}/deliveries Requires the `webhooks:write` scope. # Update a webhook endpoint Source: https://docs.meetdarwin.ai/api-reference/webhooks/update-a-webhook-endpoint /api-reference/openapi.json patch /webhook_endpoints/{id} Requires the `webhooks:write` scope. # Verify webhook signatures Source: https://docs.meetdarwin.ai/api-reference/webhooks/verify Check that a webhook came from Darwin: Standard Webhooks HMAC-SHA256 signatures, the five-minute timestamp window, code for Node and Python, Svix and the TypeScript SDK, and rotating the signing secret. Darwin signs every request the [Standard Webhooks](https://www.standardwebhooks.com/) way, so you can use an existing library or a few lines of your own. Always verify before you trust a payload: anyone can send a `POST` to your URL. 1. Take the **raw request body** exactly as received. Parsing and re-serialising the JSON changes the bytes and breaks the signature. 2. Reject the request if `webhook-timestamp` is more than five minutes from your clock. 3. Build the signed content: `` `${webhook-id}.${webhook-timestamp}.${body}` ``. 4. Compute HMAC-SHA256 over it. The key is your signing secret **without** the `whsec_` prefix, base64-decoded. 5. Base64-encode the result and compare it, in constant time, with each `v1,` entry in `webhook-signature`. The header can hold several entries separated by spaces (during a [secret rotation](#rotate-the-signing-secret)); accept if any matches. ```javascript Node (Express) theme={"theme":"vesper"} import crypto from "node:crypto"; import express from "express"; const secret = process.env.DARWIN_WEBHOOK_SECRET; // whsec_... function verify(headers, rawBody) { const id = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; const signatures = headers["webhook-signature"] || ""; if (!id || !timestamp) return false; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest("base64"); return signatures.split(" ").some((entry) => { const [version, signature] = entry.split(","); return ( version === "v1" && signature?.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)) ); }); } const app = express(); app.post("/webhooks/darwin", express.raw({ type: "application/json" }), (req, res) => { const body = req.body.toString("utf8"); if (!verify(req.headers, body)) return res.status(400).send("bad signature"); const event = JSON.parse(body); res.sendStatus(200); // answer first, work after queueForProcessing(event); // your own job queue }); app.listen(3000); ``` ```python Python (Flask) theme={"theme":"vesper"} import base64, hashlib, hmac, json, os, time from flask import Flask, request, abort secret = os.environ["DARWIN_WEBHOOK_SECRET"] # whsec_... key = base64.b64decode(secret.removeprefix("whsec_")) app = Flask(__name__) def verify(headers, body: bytes) -> bool: msg_id = headers.get("webhook-id") timestamp = headers.get("webhook-timestamp") signatures = headers.get("webhook-signature", "") if not msg_id or not timestamp or abs(time.time() - int(timestamp)) > 300: return False signed = f"{msg_id}.{timestamp}.".encode() + body expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode() return any( entry.startswith("v1,") and hmac.compare_digest(entry[3:], expected) for entry in signatures.split(" ") ) @app.post("/webhooks/darwin") def darwin_webhook(): body = request.get_data() # raw bytes if not verify(request.headers, body): abort(400) event = json.loads(body) queue_for_processing(event) # your own job queue return "", 200 ``` ### Use a Standard Webhooks library Any Standard Webhooks library verifies Darwin's signatures, including Svix's. Pass the raw body, the headers and the secret as Darwin shows it: ```javascript Node (svix) theme={"theme":"vesper"} import { Webhook } from "svix"; const wh = new Webhook(process.env.DARWIN_WEBHOOK_SECRET); // whsec_... // Throws if the signature or timestamp is wrong. const event = wh.verify(rawBody, { "webhook-id": req.headers["webhook-id"], "webhook-timestamp": req.headers["webhook-timestamp"], "webhook-signature": req.headers["webhook-signature"], }); ``` ```python Python (svix) theme={"theme":"vesper"} from svix.webhooks import Webhook wh = Webhook(os.environ["DARWIN_WEBHOOK_SECRET"]) # whsec_... event = wh.verify(raw_body, request.headers) # raises if invalid ``` With the [TypeScript SDK](/sdks/typescript/webhooks), `await Darwin.webhooks.verify(body, headers, secret)` does all of this. ## Rotate the signing secret Open the endpoint's **⋯** menu, select **Rotate signing secret** and confirm. Darwin shows the new secret once. For the next **24 hours** every delivery carries two signatures, one per secret, so your receiver keeps working while you switch it to the new secret. After 24 hours only the new secret signs. With the API: `POST /v1/webhook_endpoints/{id}/rotate_secret` returns the endpoint with its new `secret`. # The key making the request Source: https://docs.meetdarwin.ai/api-reference/workspace/the-key-making-the-request /api-reference/openapi.json get /me # The workspace, its plan and this period's credits and meetings Source: https://docs.meetdarwin.ai/api-reference/workspace/the-workspace-its-plan-and-this-periods-credits-and-meetings /api-reference/openapi.json get /workspace # Changelog Source: https://docs.meetdarwin.ai/changelog What changed in the Darwin API, webhooks, MCP server and the @meetdarwin/api TypeScript SDK. The API version is 2026-10-12; Darwin adds fields and endpoints without a new version. The API is on version `2026-10-12`. Darwin adds new fields, endpoints, events and tools without a new version, so everything below is additive unless it says otherwise. How versions work: [Versioning](/api-reference/introduction#versioning). **October 22, 2026** Opening tests from your own code ## Run opening tests yourself Your own code or AI assistant can now decide what to test on which page and write the openings, while Darwin shows each visitor one opening and scores them all. * `POST /experiments` starts a test of your own openings on a page, a page type, a page group or the whole site, against what it shows now. `GET /experiments/{id}` carries `live` results worked out now. * `PATCH` and `DELETE /experiments/{id}/variants/{variantId}` change or remove an opening. * `/page_openings`: what any page shows now and why (`GET /page_openings/current`), and a page's or section's own opening. * MCP: `create_experiment`, `edit_opening`, `remove_opening`, `get_page_opening`, `list_page_openings`, `set_page_opening`, `reset_page_opening` and `set_test_automation`. * `@meetdarwin/api` 0.3.0 adds `experiments.create`, `updateVariant`, `deleteVariant` and `pageOpenings`. Guide: [Run opening tests with your own AI assistant](/guides/opening-tests). **October 19, 2026** Outcome forms, personal links and credits ## Outcome forms and personal links in the API * `/outcomes`: build outcome forms from a template or your own questions with fit rules, and switch them live. `PATCH /outcomes/meeting` sets **Book a meeting**'s questions. `/outcome_requests` lists what visitors sent. * `/personal_links` with `POST /personal_links/{id}/urls` for each person's own address, `/link_greetings` and `/link_visits`. * New webhook event `personal_link.opened`. * New error codes `plan_required` (403, with the plan that unlocks it) and `goal_required` (409: the change would leave a live agent with no calendar or live form). * The API, the MCP server and the chat API are on every plan, Free included, with the same rate limits on every plan. * MCP: tools for outcome forms, personal links and greetings. * `@meetdarwin/api` 0.2.0 adds `outcomes`, `personalLinks`, `linkGreetings` and `linkVisits`. **October 13, 2026** Chat buttons over MCP and a new connect screen ## Chat buttons and outcome requests * MCP: `create_chat_button`, `update_chat_button`, `delete_chat_button` and `list_chat_buttons`. * Connecting an assistant shows the workspace with **Switch**, and **Full access** or **View only**. * New webhook event `outcome.submitted`, with every answer the visitor gave. **October 12, 2026** Darwin API 2026-10-12 ## The Darwin developer platform The first public version of the API, `2026-10-12`. * [REST API](/api-reference/introduction) for conversations, contacts, meetings, analytics, events, knowledge, the agent, its appearance, chat buttons and opening tests, with scoped keys, cursor pagination and idempotent writes. * [Webhooks](/api-reference/webhooks) signed with Standard Webhooks, with retries over three days and redelivery. * The [browser SDK](/api-reference/browser-sdk), the [chat API](/api-reference/chat-api) and the [MCP server](/api-reference/mcp) for Claude, ChatGPT, Claude Code and Cursor. * [Privacy requests](/api-reference/privacy): export or erase one person's data. * `@meetdarwin/api` 0.1.0, the [TypeScript SDK](/sdks/typescript). # Sync leads to a CRM Source: https://docs.meetdarwin.ai/guides/crm-sync Copy every contact Darwin captures into your CRM on a schedule with updated_since and cursor pagination, and write the CRM's id back as external_id. Copy every contact Darwin captures into your CRM every 15 minutes, and record Darwin's contact id back so later runs update the same record. **You need:** a key with `contacts:read` (and `contacts:write` if you write the CRM's id back), and a place to keep one timestamp between runs. ```javascript sync-contacts.mjs theme={"theme":"vesper"} const API = "https://api.meetdarwin.ai/v1"; const headers = { Authorization: `Bearer ${process.env.DARWIN_API_KEY}`, "Content-Type": "application/json" }; export async function syncContacts(state, crm) { const started = new Date().toISOString(); // 1. note the time before reading let cursor = null; for (;;) { const url = new URL(`${API}/contacts`); url.searchParams.set("limit", "100"); if (state.lastRun) url.searchParams.set("updated_since", state.lastRun); if (cursor) url.searchParams.set("starting_after", cursor); const res = await fetch(url, { headers }); if (res.status === 429) { await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after")) * 1000)); continue; // retry the same page } if (!res.ok) throw new Error(`Darwin ${res.status}: ${(await res.json()).code}`); const page = await res.json(); for (const contact of page.data) { // 2. upsert by email; the same contact can arrive in two runs const crmId = await crm.upsertPerson({ email: contact.email, firstName: contact.first_name, lastName: contact.last_name, company: contact.company, source: `Darwin (${contact.source})`, }); // 3. optional: remember the CRM id in Darwin if (!contact.external_id) { await fetch(`${API}/contacts/${contact.id}`, { method: "PATCH", headers, body: JSON.stringify({ external_id: String(crmId) }), }); } } if (!page.has_more) break; cursor = page.next_cursor; } state.lastRun = started; // 4. only after every page succeeded } ``` Run it on a schedule (cron, a scheduled function, or your integration platform). The first run, with no `lastRun`, copies every contact. * Writing `external_id` changes the contact, so it comes back once in the next run. The `if (!contact.external_id)` check makes that second pass a no-op. * Each contact can have one `external_id`, unique in the workspace. Two contacts with the same id return `409` [`conflict`](/api-reference/errors#conflict). * For the conversation behind a lead, list `GET /v1/conversations?has_lead=true&updated_since=…` and match on `contact`. * If you'd rather not poll, subscribe a webhook to `lead.captured` and upsert the contact in `data.object`. Keep this job as a nightly safety net for any webhook you miss. **It worked if** a test lead from your live chat appears in the CRM after the next run, and `GET /v1/contacts?email=…` shows the CRM's id as `external_id`. # Tell the rep in Slack when a prospect opens their link Source: https://docs.meetdarwin.ai/guides/link-opened-alerts Post to Slack the moment a prospect opens their Darwin personal link, with the personal_link.opened webhook. Post to Slack the moment someone opens their personal link, whether or not they chat, so the rep can follow up while they're on the site. **You need:** the Slack function from [Send handoffs to Slack](/guides/slack-handoffs), subscribed to `personal_link.opened` instead. ```javascript theme={"theme":"vesper"} // In the same server, after the signature check: if (event.type === "personal_link.opened" && !seen.has(event.id)) { seen.add(event.id); const visit = event.data.object; if (!visit) return; const who = [visit.person.first_name, visit.person.last_name].filter(Boolean).join(" ") || visit.person.email || "Someone"; const from = visit.person.company ? ` from ${visit.person.company}` : ""; await fetch(SLACK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: `:eyes: ${who}${from} opened "${visit.link?.name ?? "a personal link"}" and is on ${visit.page}` }), }); } ``` Darwin sends one `personal_link.opened` per browser and link within 12 hours, so a prospect clicking through three pages is one message. Clicks from your team and test browsers never send it. With Zapier, use the **Personal Link Opened** trigger and pick one link to hear about only that campaign. # Run opening tests with your own AI assistant Source: https://docs.meetdarwin.ai/guides/opening-tests Let Claude, ChatGPT, Cursor or your own code choose what to test on which page, write the openings and read the results, while Darwin scores every test. Let Claude, ChatGPT, Cursor or your own code decide what to test on which page, write the openings and read the results, while Darwin shows each visitor one opening and scores them all on visitors who reach booking. **You need:** an assistant connected to Darwin with **Full access** (see [MCP server](/api-reference/mcp)), or a key with `experiments:write` and `agent:write`. **1. Leave new tests to your assistant** Ask your assistant to *"stop Darwin starting tests on its own"* (`set_test_automation`), or switch off **Darwin starts new tests** in [Experiments → Settings](https://www.meetdarwin.ai/help/experiments/automatic-testing). Keep **Run tests automatically** on: it is what scores the tests. With the API: `PATCH /v1/agent` with `{"automation": {"auto_launch": false}}`. **2. Look before you test** *"What does our pricing page say first?"* uses `get_page_opening`, which answers with the opening, whether a test is already running there, and whether it comes from a greeting, the page's own opening or the site's. A greeting comes before a page's opening; `list_greetings` shows them all. `GET /v1/analytics` has each page's visitors and meetings, for choosing where to test. **3. Start a test** *"Test an opening on /pricing that asks for their team size first."* The assistant writes the openings and calls `create_experiment`. With the API: ```bash theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/experiments \ -H "Authorization: Bearer $DARWIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Pricing: ask the team size", "page": "/pricing", "openings": [ { "headline": "How many people would use it? I will show you the plan that fits.", "buttons": [{ "label": "Under 10" }, { "label": "10 to 50" }, { "label": "Over 50" }] } ] }' ``` Darwin adds the opening the page shows now as the current one, so your openings are compared with what visitors see today. Test a whole page type with `"unit": "type:pricing"`, a page group with `"unit": "group:…"`, or every page on the site's opening with `"unit": "site"`. One test per page at a time. **4. Read the results** *"How is the pricing test doing?"* uses `get_experiment`: each opening's views, conversations, visitors who reached booking, meetings, chance to beat the current opening and share of visitors, a sentence on where the test stands, and what a new opening still needs before it can be applied. `GET /v1/experiments/exp_…` returns the same under `live`. Reading results saves nothing. **5. Change, apply or stop** `edit_opening` and `remove_opening` change a running test; new words start as a fresh opening. `apply_winner` makes the winner the page's opening once it has the evidence Darwin requires; `stop_experiment` ends a test without one. To change a page's opening without a test, use `set_page_opening` (a page, or a section such as `/blog/*`), which is refused while a test runs on that page. A page with a greeting of its own gets the same words on that greeting; if another greeting still speaks for the page, the answer names it in `overridden_by` and `update_greeting` changes it. To test a greeting itself, pass `greeting` (gr\_…) to `create_experiment`. Tests your assistant starts appear in Experiments like any other, marked as written through the API or by an AI assistant, and the `experiment.started` and `experiment.winner_found` [webhooks](/api-reference/webhooks) fire for them. **It worked if** `get_page_opening` for the page shows `source: test` with your test's id, and the test appears in Experiments. # Give every prospect their own personal link Source: https://docs.meetdarwin.ai/guides/personal-links Make each prospect's own Darwin personal link with the API and store it on their CRM record, so any email or sequence can greet them by name. Put each prospect's own [personal link](https://www.meetdarwin.ai/help/links/personal-links) on their CRM record, so any email or sequence can use it. When they click, the chat greets them by name and the agent knows who they are. **You need:** a key with `agent:read`, and a personal link (**Links**, or `POST /v1/personal_links`). Give it a greeting if you want the chat to greet each person by name; without one, the link opens the page as usual and the agent still knows who arrived. Its id, `lk_…`, is in `GET /v1/personal_links`. ```javascript personal-links.mjs theme={"theme":"vesper"} const API = "https://api.meetdarwin.ai/v1"; const headers = { Authorization: `Bearer ${process.env.DARWIN_API_KEY}`, "Content-Type": "application/json" }; const LINK = process.env.DARWIN_LINK_ID; // lk_... export async function addPersonalLinks(prospects, crm) { // Up to 1,000 people per call; addresses come back in the same order. for (let i = 0; i < prospects.length; i += 1000) { const batch = prospects.slice(i, i + 1000); const res = await fetch(`${API}/personal_links/${LINK}/urls`, { method: "POST", headers, body: JSON.stringify({ people: batch.map((p) => ({ email: p.email, first_name: p.firstName, last_name: p.lastName, company: p.company, title: p.jobTitle, external_id: p.crmId, // returned with the address })), }), }); if (!res.ok) throw new Error(`Darwin ${res.status}: ${(await res.json()).detail}`); for (const made of (await res.json()).data) { await crm.updatePerson(made.external_id, { darwin_link: made.url }); if (made.dropped.length) console.warn(made.external_id, "details left out:", made.dropped); } } } ``` Each address carries the person's details already filled in and encoded, so an email such as `jane+q4@acme.com` or a company such as `Smith & Sons` arrives intact. Nothing is stored in Darwin until someone clicks; then they become a contact, like a form submission. * `greeting` in each result is what the chat will say to that person, and `missing` names the details their greeting uses but they lack. Check a few before a large send. * A change to the link's greeting reaches every address you already made, because the greeting is looked up by the link's id. * No code: Zapier's **Create Personal Link URL** action does the same for one person per run, for example when a contact enters a HubSpot list. See [Connect Zapier](https://www.meetdarwin.ai/help/crm-and-automation/zapier). **It worked if** opening one of the addresses on your live site shows that person's greeting in the chat, and `GET /v1/link_visits?link=lk_…` lists the visit. # Create a quote form with the API Source: https://docs.meetdarwin.ai/guides/quote-form Create a Darwin outcome form such as a quote request with the API, test it and switch it live, without opening the form builder. Create an [outcome form](https://www.meetdarwin.ai/help/outcomes/what-are-outcomes) such as a quote request, and switch it on, without opening the form builder. Useful when you set up many workspaces the same way, or keep forms in version control. **You need:** a key with `agent:write`. ```bash theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/outcomes \ -H "Authorization: Bearer $DARWIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Request a quote", "offer_when": "asks what a project would cost or wants an estimate", "questions": [ { "question": "What do you need?", "type": "short_text" }, { "question": "How many pages does your site have?", "type": "number", "unit": "pages", "min": 5 }, { "question": "When do you want it done?", "type": "choice", "choices": ["This month", "This quarter", "Just planning"] } ], "phone": "optional", "done_message": "Thanks. We will send your quote within one business day.", "notify": ["sales@example.com"], "status": "draft" }' ``` The answer is the form, with an id (`out_…`) and each question's `id` and answer `key`. Try it in the preview on the **Agent** page, then switch it on with `PATCH /v1/outcomes/out_… {"status": "live"}`. A request a visitor sends through a live form uses a credit like a booked meeting, or nothing when it doesn't pass the fit rules; see [How requests use credits](https://www.meetdarwin.ai/help/outcomes/how-requests-use-credits). On Free, a form can have one qualifying question, a question with a fit rule; a write with more returns `403` [`plan_required`](/api-reference/errors#plan_required). * `"template": "quote"` or `"callback"` starts from Darwin's templates; anything else you send replaces the template's. * `PATCH /v1/outcomes/meeting` sets the questions asked before the calendar on **Book a meeting**, with the same fit rules (`not_a_fit`, `min`, `max`). * When you change questions, send each one you keep with its `id`, so its earlier answers stay matched. A question's `key` comes from its words, so rewording it changes the key Zapier and webhooks use. **It worked if** `GET /v1/outcomes` lists the form as `live`, and asking the preview on the **Agent** page for a quote opens it in the chat. # Send handoffs to Slack Source: https://docs.meetdarwin.ai/guides/slack-handoffs Post to a Slack channel within seconds when a visitor asks the Darwin chat for a person: a complete webhook receiver in Node with signature checks and de-duplication. When a visitor asks to talk to a person, post it to a Slack channel within seconds, with the page they were on and a link to the conversation. **You need:** a Slack [incoming webhook URL](https://api.slack.com/messaging/webhooks) for the channel, and somewhere to run a small HTTPS function (a serverless function, a container or an existing server). ```javascript server.mjs theme={"theme":"vesper"} import crypto from "node:crypto"; import http from "node:http"; const DARWIN_SECRET = process.env.DARWIN_WEBHOOK_SECRET; // whsec_... from Settings → Integrations → API const SLACK_URL = process.env.SLACK_WEBHOOK_URL; const seen = new Set(); // use a database or cache in production function verify(headers, body) { const id = headers["webhook-id"], ts = headers["webhook-timestamp"]; if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const key = Buffer.from(DARWIN_SECRET.replace(/^whsec_/, ""), "base64"); const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest("base64"); return (headers["webhook-signature"] || "").split(" ").some((entry) => { const sig = entry.split(",")[1] || ""; return sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); }); } http .createServer((req, res) => { let body = ""; req.on("data", (chunk) => (body += chunk)); req.on("end", async () => { if (req.method !== "POST" || !verify(req.headers, body)) return res.writeHead(400).end(); res.writeHead(200).end(); // answer Darwin first const event = JSON.parse(body); if (event.type !== "handoff.requested" || seen.has(event.id)) return; seen.add(event.id); const c = event.data.object; if (!c) return; const who = [c.lead.first_name, c.lead.last_name].filter(Boolean).join(" ") || c.lead.email || "A visitor"; await fetch(SLACK_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: `:raising_hand: ${who} asked for a person on ${c.page_path ?? "your site"}\n>${c.handoff?.reason ?? c.preview ?? ""}\n`, }), }); }); }) .listen(process.env.PORT || 3000); ``` **1. Deploy the function** Deploy it so it is reachable at an HTTPS address, for example `https://hooks.example.com/darwin`. Set `SLACK_WEBHOOK_URL` now; you'll get `DARWIN_WEBHOOK_SECRET` in the next step. **2. Add the endpoint in Darwin** In **Settings → Integrations → API → Webhooks**, select **Add endpoint**, paste the address, choose only `handoff.requested`, and select **Add endpoint**. Copy the signing secret into `DARWIN_WEBHOOK_SECRET` and redeploy. **3. Test it** Select **Send test event** in the endpoint's menu. Darwin should report a `200`. The test event's type is `webhook.test`, so nothing is posted to Slack. **It worked if**, after someone asks the chat on your live site to talk to a person, a message appears in the channel within a few seconds. The same pattern sends `meeting.booked` to a sales channel or `lead.captured` to an SDR queue: subscribe to the type and read `data.object`. See [Webhooks](/api-reference/webhooks) for every payload. # Export conversations to your warehouse nightly Source: https://docs.meetdarwin.ai/guides/warehouse-export Export every Darwin conversation with its transcript and grading to BigQuery, Snowflake, Redshift or files each night, and the fields worth loading. Load every conversation, with its transcript and Darwin's grading, into BigQuery, Snowflake, Redshift or plain files each night. **You need:** a key with `conversations:read`. Leave out `contacts:read` if the warehouse should not hold names and emails: lead and booker fields then come back `null` (transcripts still hold whatever visitors typed). ```javascript export-conversations.mjs theme={"theme":"vesper"} import { createWriteStream } from "node:fs"; const API = "https://api.meetdarwin.ai/v1"; const headers = { Authorization: `Bearer ${process.env.DARWIN_API_KEY}` }; // A rolling three-day window: Darwin grades a conversation after it goes quiet, // so re-export recent days and let the warehouse upsert by id. const since = new Date(Date.now() - 3 * 86400_000).toISOString(); const file = createWriteStream(`conversations-${new Date().toISOString().slice(0, 10)}.ndjson`); let cursor = null; for (;;) { const url = new URL(`${API}/conversations`); url.searchParams.set("limit", "100"); url.searchParams.set("created_since", since); url.searchParams.set("expand", "messages"); if (cursor) url.searchParams.set("starting_after", cursor); const res = await fetch(url, { headers }); if (res.status === 429) { await new Promise((r) => setTimeout(r, Number(res.headers.get("retry-after")) * 1000)); continue; } if (!res.ok) throw new Error(`Darwin ${res.status}: ${(await res.json()).code}`); const page = await res.json(); for (const conversation of page.data) file.write(JSON.stringify(conversation) + "\n"); if (!page.has_more) break; cursor = page.next_cursor; } file.end(); ``` Load the file into a table keyed on `id`, merging over yesterday's rows. Useful columns: | Field | Use it for | | - | - | | `created_at`, `page_path`, `visitor.source`, `visitor.utm`, `visitor.ai_referred` | Where conversations come from | | `outcome` (`meeting`, `lead`, `handoff`, `none`) | Results by page and source | | `evaluation.intent`, `evaluation.buying_stage`, `evaluation.qualification_score` | Who is in market | | `evaluation.understood`, `evaluation.failure_category`, `evaluation.recommended_improvement` | Where the agent needs better knowledge | | `opening.experiment`, `opening.variant` | Which opening the visitor saw | | `messages` | Full-text analysis | Add `GET /v1/meetings?updated_since=…` and `GET /v1/analytics?range=7d` to the same job for meetings and daily funnel numbers. **It worked if** the morning's file has one line per conversation from the last three days and yesterday's graded conversations have a non-null `evaluation`. # Darwin developer docs Source: https://docs.meetdarwin.ai/index Build on Darwin: the REST API, signed webhooks, the TypeScript and browser SDKs, the chat API and the MCP server for Claude, ChatGPT and Cursor. On every plan, Free included. \[ Developers ] # Build on Darwin. Connect Darwin to your stack with the REST API, webhooks, the SDKs or the MCP server. On every plan, Free included. [Quickstart](/quickstart) [API reference](/api-reference/introduction) ![The Darwin D mark drawn as a wireframe solid on construction lines.](https://mintcdn.com/meet-darwin/mTVeJvfUDOO_84GO/images/dev/hero-mark.svg?fit=max\&auto=format\&n=mTVeJvfUDOO_84GO\&q=85\&s=fdca7ffbfd71953bc73433f890a46db8) ## Every way in. One key for the REST API, webhooks, the chat API and the MCP server. [\[ 01 \]**REST API**`GET /v1/conversations`Read conversations, contacts, meetings and requests, and change the agent from code.](/api-reference/introduction) [\[ 02 \]**Webhooks**`meeting.booked`A signed POST the moment a lead, request or meeting comes in.](/api-reference/webhooks) [\[ 03 \]**TypeScript SDK**`npm i @meetdarwin/api`Typed calls with retries, idempotency and pagination built in.](/sdks/typescript) [\[ 04 \]**Browser SDK**`Darwin("open")`Open the chat from your own buttons and listen for leads and bookings.](/api-reference/browser-sdk) [\[ 05 \]**Chat API**`POST /v1/chat/sessions`Put your website's agent behind your own app, Slack or voice bot.](/api-reference/chat-api) [\[ 06 \]**MCP server**`app.meetdarwin.ai/api/mcp`Let Claude, ChatGPT or Cursor read results and manage the agent.](/api-reference/mcp) ## Your first call. Create a key, then ask Darwin who it belongs to.
  1. 01Create a key with **Read only** access
  2. 02Call `GET /v1/me` to check it
  3. 03List the leads your chat captured
[Follow the quickstart](/quickstart) [Get an API key](https://app.meetdarwin.ai/admin/settings/integrations/api) ```bash Request theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/me \ -H "Authorization: Bearer $DARWIN_API_KEY" ``` ```json Response theme={"theme":"vesper"} { "object": "api_key_identity", "name": "Warehouse sync", "mode": "live", "scopes": ["contacts:read", "conversations:read"], "workspace": "6f1c2b9e-4d1a-4f6b-9a3e-2c7d8e9f0a1b" } ``` ## Guides. Complete examples of common jobs. [01**Send handoffs to Slack**Webhooks](/guides/slack-handoffs) [02**Sync leads to a CRM**REST API](/guides/crm-sync) [03**Export conversations to your warehouse nightly**REST API](/guides/warehouse-export) [04**Give every prospect their own personal link**REST API](/guides/personal-links) [05**Tell the rep in Slack when a prospect opens their link**Webhooks](/guides/link-opened-alerts) [06**Create a quote form with the API**REST API](/guides/quote-form) [07**Run opening tests with your own AI assistant**MCP · REST API](/guides/opening-tests) Concepts [Authentication and scopes](/api-reference/authentication) [Errors](/api-reference/errors) [Pagination and syncing](/api-reference/pagination) [Idempotency](/api-reference/idempotency) [Rate limits](/api-reference/rate-limits) [Versioning](/api-reference/introduction#versioning) Webhooks [Events and payloads](/api-reference/webhooks/events) [Verify signatures](/api-reference/webhooks/verify) [Delivery and retries](/api-reference/webhooks/delivery) [Privacy requests](/api-reference/privacy) Resources [Changelog](/changelog) [OpenAPI spec](https://app.meetdarwin.ai/api/v1/openapi.json) [@meetdarwin/api on npm](https://www.npmjs.com/package/@meetdarwin/api) [Help center](https://www.meetdarwin.ai/help) ![Darwin](https://mintcdn.com/meet-darwin/uC-9C8ZEQcozt46p/images/brand/logo-dark.svg?fit=max\&auto=format\&n=uC-9C8ZEQcozt46p\&q=85\&s=750d5ed6a7aab68a573555066743a25a) [Website](https://www.meetdarwin.ai) [Developers](https://www.meetdarwin.ai/developers) [Pricing](https://www.meetdarwin.ai/pricing) [Log in](https://app.meetdarwin.ai/login) # Quickstart Source: https://docs.meetdarwin.ai/quickstart Make your first Darwin API call in five minutes: create a key, check it with GET /me, read the leads your chat captured, and get a signed webhook when a meeting is booked. curl, TypeScript and Python. From a new key to your first webhook in four steps. You need a Darwin workspace on any plan, Free included, and to be its owner or admin. **1. Create a key** In Darwin, open **Settings → Integrations → API** and select **Create key**. Name it after where it will run, choose **Read only** for now, and copy it: Darwin shows it once. ```bash theme={"theme":"vesper"} export DARWIN_API_KEY="dk_live_..." ``` Keys, prefixes and every scope: [Authentication](/api-reference/authentication). **2. Check the key** `GET /me` needs no scope and says which workspace and scopes the key has. ```bash curl theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/me \ -H "Authorization: Bearer $DARWIN_API_KEY" ``` ```ts TypeScript theme={"theme":"vesper"} import { Darwin } from "@meetdarwin/api"; // npm install @meetdarwin/api const darwin = new Darwin(); // reads DARWIN_API_KEY console.log(await darwin.me()); ``` ```python Python theme={"theme":"vesper"} import os, requests res = requests.get( "https://api.meetdarwin.ai/v1/me", headers={"Authorization": f"Bearer {os.environ['DARWIN_API_KEY']}"}, ) print(res.status_code, res.json()) ``` **It worked if** you get `200` with `"object": "api_key_identity"`, the key's `scopes` and your `workspace`. **3. Read the leads your chat captured** List conversations where the visitor left their details. Names and emails need `contacts:read`, which **Read only** includes. ```bash curl theme={"theme":"vesper"} curl "https://api.meetdarwin.ai/v1/conversations?has_lead=true&limit=5" \ -H "Authorization: Bearer $DARWIN_API_KEY" ``` ```ts TypeScript theme={"theme":"vesper"} for await (const conversation of darwin.conversations.list({ has_lead: true })) { console.log(conversation.lead.email, conversation.outcome, conversation.page_path); } ``` Lists share one shape, `{ "object": "list", "data", "has_more", "next_cursor" }`. See [Pagination](/api-reference/pagination). **4. Get a webhook when a meeting is booked** Create a key with `webhooks:write` (or use **Full access**), then point Darwin at an HTTPS address of yours: ```bash curl theme={"theme":"vesper"} curl https://api.meetdarwin.ai/v1/webhook_endpoints \ -H "Authorization: Bearer $DARWIN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/webhooks/darwin", "events": ["meeting.booked", "lead.captured"] }' ``` ```ts TypeScript theme={"theme":"vesper"} const endpoint = await darwin.webhookEndpoints.create({ url: "https://example.com/webhooks/darwin", events: ["meeting.booked", "lead.captured"], }); console.log(endpoint.secret); // whsec_… shown only now; store it ``` Store the `secret` from the response, [verify every delivery](/api-reference/webhooks/verify) with it, and send a test with `POST /v1/webhook_endpoints/{id}/test`. **It worked if** the test response shows your endpoint answered `2xx`. ## Next > [card] [REST API](/api-reference/introduction) > > Base URL, requests, request ids and versioning. > [card] [Webhooks](/api-reference/webhooks) > > Events, payloads, signatures and retries. > [card] [TypeScript SDK](/sdks/typescript) > > Typed calls with retries and pagination built in. > [card] [Guides](/api-reference/recipes) > > Slack alerts, CRM sync, a nightly warehouse export and more. # TypeScript SDK Source: https://docs.meetdarwin.ai/sdks/typescript Install @meetdarwin/api, the typed TypeScript client for the Darwin REST API: conversations, contacts, meetings, outcome forms, personal links, webhooks, knowledge, the agent and the chat API, with retries, idempotency and pagination built in. `@meetdarwin/api` is the TypeScript client for the [Darwin REST API](/api-reference/introduction). It covers every resource in the API, with types for every object and parameter. * No runtime dependencies. It uses the global `fetch`, so it runs on Node 18+, Deno, Bun, Cloudflare Workers and Vercel Edge. * ESM, with type declarations for every object and parameter. * Retries, idempotency keys, cursor pagination and webhook verification built in. ## Install ```bash npm theme={"theme":"vesper"} npm install @meetdarwin/api ``` ```bash pnpm theme={"theme":"vesper"} pnpm add @meetdarwin/api ``` ```bash yarn theme={"theme":"vesper"} yarn add @meetdarwin/api ``` ## Quickstart Create a secret key in Darwin under **Settings → Integrations → API** ([how](/api-reference/authentication)). Keep it on your server. ```ts quickstart.ts theme={"theme":"vesper"} import { Darwin } from "@meetdarwin/api"; const darwin = new Darwin({ apiKey: process.env.DARWIN_API_KEY }); // Every lead captured since October 1, across every page. for await (const conversation of darwin.conversations.list({ has_lead: true, created_since: new Date("2026-10-01"), })) { console.log(conversation.lead.email, conversation.outcome, conversation.page_path); } // Create or update a contact, keyed by email. const contact = await darwin.contacts.create({ email: "ada@example.com", first_name: "Ada", company: "Analytical Engines", external_id: "crm_00123", attributes: { plan: "enterprise", seats: 40 }, }); // Get a signed POST to your URL when a meeting is booked. const endpoint = await darwin.webhookEndpoints.create({ url: "https://example.com/webhooks/darwin", events: ["meeting.booked", "lead.captured"], }); console.log(endpoint.secret); // whsec_… shown only now; store it ``` `new Darwin()` with no `apiKey` reads the `DARWIN_API_KEY` environment variable. > [!WARNING] > > The client refuses to start in a browser, because a secret key there is visible to every visitor. Call Darwin from your server. To control the chat on your website, use the [Browser SDK](/api-reference/browser-sdk). ## Options ```ts theme={"theme":"vesper"} new Darwin({ apiKey: "dk_live_…", // or dk_test_… baseUrl: "https://api.meetdarwin.ai/v1", // default; also served at https://app.meetdarwin.ai/api/v1 version: "2026-10-12", // Darwin-Version header; defaults to the version these types describe timeoutMs: 30_000, // per attempt maxRetries: 2, // retries after the first attempt fetch: customFetch, // defaults to globalThis.fetch }); ``` Every method also takes a last `options` argument for that one call: `{ idempotencyKey, signal, timeoutMs, maxRetries, headers }`. ## Resources Parameters and fields use the API's own names (`has_lead`, `created_since`, `next_cursor`), so the [API reference](/api-reference/introduction) applies word for word. Anywhere a parameter takes a time you may pass a `Date` or an ISO string. Responses carry timestamps as ISO strings. | Namespace | Methods | | - | - | | `darwin.me()` | The key: name, mode, scopes, workspace | | `darwin.workspace` | `get` | | `darwin.conversations` | `list`, `retrieve` | | `darwin.contacts` | `list`, `create` (upsert by email; also `upsert`), `retrieve`, `update`, `erase` | | `darwin.meetings` | `list`, `retrieve` | | `darwin.outcomes` | `list`, `create`, `retrieve`, `update`, `delete` (`"meeting"` is Book a meeting) | | `darwin.outcomeRequests` | `list`, `retrieve` | | `darwin.analytics` | `get` | | `darwin.events` | `list`, `retrieve`, `redeliver` | | `darwin.webhookEndpoints` | `list`, `create`, `retrieve`, `update`, `delete`, `test`, `rotateSecret`, `deliveries` | | `darwin.knowledge.sources` | `list`, `add`, `retrieve`, `delete`, `refresh` | | `darwin.knowledge.documents` | `list`, `create`, `retrieve`, `replace`, `delete` | | `darwin.agent` | `get`, `update` | | `darwin.appearance` | `get`, `update` | | `darwin.chatButtons` | `list`, `create`, `retrieve`, `update`, `delete` | | `darwin.personalLinks` | `list`, `create`, `retrieve`, `update`, `delete`, `urls` (each person's own address) | | `darwin.linkGreetings` | `list`, `create`, `retrieve`, `update`, `delete` | | `darwin.linkVisits` | `list`, `retrieve` | | `darwin.experiments` | `list`, `create`, `retrieve` (with live results), `pause`, `resume`, `stop`, `promote`, `addVariant`, `updateVariant`, `deleteVariant` | | `darwin.pageOpenings` | `list`, `current`, `set`, `retrieve`, `delete` | | `darwin.privacy` | `erase`, `export` | | `darwin.chat.sessions` | `create`, `retrieve`, `send`, `sendStream` | For an endpoint this version does not wrap yet, `darwin.request(method, path, { query, body })` gets the same auth, retries and errors. ## Requirements Node 18.17 or later, or any runtime with `fetch`, Web Crypto and `ReadableStream`. MIT licensed. Release notes are in the [changelog](/changelog). ## Next > [card] [Pagination](/sdks/typescript/pagination) > > One page, every item, or whole pages with a ceiling. > [card] [Errors and retries](/sdks/typescript/errors) > > Error classes, automatic retries and idempotency keys. > [card] [Verify webhooks](/sdks/typescript/webhooks) > > Express and Next.js receivers in a few lines. > [card] [Stream chat replies](/sdks/typescript/chat) > > Talk to your agent and stream its answer. # Stream chat replies with the TypeScript SDK Source: https://docs.meetdarwin.ai/sdks/typescript/chat Talk to your Darwin agent from your own app with @meetdarwin/api: create a chat session, send a message and wait for the reply, or stream it with sendStream and textDeltas. Talk to your Darwin agent from your own app, Slack, WhatsApp or a voice bot. The key needs the `chat:write` scope. Darwin keeps the transcript; you send only what the visitor said. How sessions, actions and allowances work: [Chat API](/api-reference/chat-api). ```ts theme={"theme":"vesper"} const session = await darwin.chat.sessions.create({ visitor: { email: "ada@example.com", name: "Ada" }, // treated as verified: you hold the key channel: "app", context: { plan: "enterprise", seats: 40 }, }); // Wait for the whole reply. const reply = await darwin.chat.sessions.send(session.id, { text: "Can I book a demo?" }); console.log(reply.message.text); for (const action of reply.actions) { if (action.type === "booking_offered") showBookingButton(action.booking_url); } // Or stream it as it is written. const stream = await darwin.chat.sessions.sendStream(session.id, { text: "What does it cost?" }); for await (const delta of stream.textDeltas()) process.stdout.write(delta); ``` A `ChatStream` can also be iterated for every parsed chunk of the AI SDK UI message stream (`start`, `text-delta`, `tool-input-available`, `tool-output-available`, `finish`, `error`…), or read in one go with `await stream.finalText()`. Call `stream.abort()` to stop. `sendStream` sends no automatic `Idempotency-Key`, because the API holds an idempotent response back until it is complete in order to store it. For the same reason it is retried only after a 429. `send` gets a key and the usual retries. # Errors and retries in the TypeScript SDK Source: https://docs.meetdarwin.ai/sdks/typescript/errors Handle Darwin API errors in @meetdarwin/api: DarwinError and its classes by status, automatic retries on 429, 5xx and network errors, idempotency keys on every write, and cancelling a call. ## Errors Every failure is a `DarwinError`. HTTP errors carry the API's [problem document](/api-reference/errors): ```ts theme={"theme":"vesper"} import { DarwinError, NotFoundError, RateLimitError } from "@meetdarwin/api"; try { await darwin.contacts.retrieve("ct_missing"); } catch (error) { if (error instanceof NotFoundError) { // 404 } else if (error instanceof DarwinError) { console.error(error.status, error.code, error.detail, error.param, error.requestId); } } ``` | Field | Meaning | | - | - | | `status` | HTTP status; `undefined` for network errors and timeouts | | `code` | Stable machine-readable code, such as `insufficient_scope`. Branch on this. | | `detail` | A sentence for a person | | `param` | The field at fault, such as `limit` or `buttons[0].label` | | `requestId` | Quote it to Darwin support | | `requiredPlan` | With `plan_required` (403): the plan that unlocks it, `growth` or `agency` | | `headers` | The response headers (`RateLimit-*`, `Retry-After`, `WWW-Authenticate`) | Classes by status: `InvalidRequestError` (400), `AuthenticationError` (401), `PaymentRequiredError` (402), `PermissionDeniedError` (403), `NotFoundError` (404), `ConflictError` (409), `UnprocessableError` (422), `RateLimitError` (429, with `retryAfter` in seconds), `InternalServerError` (5xx), `ConnectionError` and its subclass `TimeoutError` (no response). Error messages never contain your API key. ## Retries The client retries, up to `maxRetries` times (default 2): * **429**: after the `Retry-After` delay. If the API asks for more than a minute, the client gives up at once with a `RateLimitError` so you can schedule the work. * **5xx and network errors**: with exponential backoff and jitter (about 0.5 s, 1 s, 2 s…). * **409 `idempotency_key_in_use`**: an earlier attempt is still running; the retry gets its answer. ## Idempotency Every `POST`, `PATCH` and `DELETE` carries an `Idempotency-Key`. The client makes up a random UUID when you do not pass one and reuses it on every retry, so a retried write is applied once. The API keeps a key's response for 24 hours; a replayed response has the `Idempotent-Replayed: true` header. Pass your own key when a write may be repeated across processes or restarts, such as a queue job: ```ts theme={"theme":"vesper"} await darwin.contacts.create({ email: "ada@example.com" }, { idempotencyKey: `signup-${userId}` }); ``` Reusing a key with a different body is refused with `422 idempotency_key_reused`. More in [Idempotency](/api-reference/idempotency). ## Cancel a call Cancel a call, and its retry waits, with an `AbortSignal`: ```ts theme={"theme":"vesper"} await darwin.analytics.get({ range: "30d" }, { signal: AbortSignal.timeout(5_000) }); ``` # Pagination in the TypeScript SDK Source: https://docs.meetdarwin.ai/sdks/typescript/pagination Page through Darwin API lists with @meetdarwin/api: await one page, iterate every item with for await, read whole pages, or collect an array with a ceiling, and sync only what changed. Every `list` method returns a `PagePromise`. Await it for one page; iterate it with `for await` for every item across all pages. ```ts theme={"theme":"vesper"} // One page: { object: "list", data, has_more, next_cursor } const page = await darwin.contacts.list({ limit: 100 }); // Every contact changed since the last sync, fetching pages as needed. for await (const contact of darwin.contacts.list({ limit: 100, updated_since: lastSync })) { await saveToCrm(contact); } // Whole pages, or an array with a ceiling (default 10,000). for await (const page of darwin.meetings.list().pages()) console.log(page.data.length); const recent = await darwin.events.list({ type: "meeting.booked" }).toArray({ max: 500 }); ``` Nothing is sent until you await or iterate. The filters you pass are repeated on every page. To resume by hand, pass a page's `next_cursor` as `starting_after`. ## Sync only what changed A sync job should record the time it starts, not the time it ends, and pass that as `updated_since` next run: records that change while you page can move ahead of your cursor. ```ts theme={"theme":"vesper"} const started = new Date(); for await (const contact of darwin.contacts.list({ limit: 100, updated_since: state.lastRun })) { await crm.upsertPerson(contact); } state.lastRun = started; // only after every page succeeded ``` How cursors, ordering and `updated_since` work in the API itself: [Pagination and syncing](/api-reference/pagination). # Verify webhooks with the TypeScript SDK Source: https://docs.meetdarwin.ai/sdks/typescript/webhooks Verify Darwin webhook signatures with Darwin.webhooks.verify in @meetdarwin/api: Express and Next.js route handlers, typed event payloads, local testing with sign, and secret rotation. Darwin signs every delivery with [Standard Webhooks](https://www.standardwebhooks.com/). `Darwin.webhooks.verify(payload, headers, secret, toleranceSeconds = 300)` checks the signature and the timestamp and returns the parsed event, or throws a `WebhookVerificationError` whose `reason` says why. > [!NOTE] > > **`verify` is async** (it returns a Promise), because it uses Web Crypto so that it runs the same on Node and edge runtimes. Always `await` it. Pass the **raw** request body, exactly as received. A body that was parsed and turned back into JSON will not verify. Respond with a 2xx quickly and do slow work afterwards. Darwin retries failed deliveries with the same `webhook-id`, so dedupe on `event.id`. See [Delivery and retries](/api-reference/webhooks/delivery). ```ts Express theme={"theme":"vesper"} import express from "express"; import { Darwin, WebhookVerificationError } from "@meetdarwin/api"; const app = express(); app.post("/webhooks/darwin", express.raw({ type: "application/json" }), async (req, res) => { let event; try { event = await Darwin.webhooks.verify(req.body, req.headers, process.env.DARWIN_WEBHOOK_SECRET!); } catch (error) { if (error instanceof WebhookVerificationError) return res.status(400).send(error.message); throw error; } switch (event.type) { case "meeting.booked": console.log("Booked:", event.data.object?.invitee.email, event.data.object?.start_at); break; case "lead.captured": console.log("Lead:", event.data.object?.email); break; } res.sendStatus(200); }); ``` ```ts Next.js theme={"theme":"vesper"} // app/api/webhooks/darwin/route.ts import { Darwin, WebhookVerificationError } from "@meetdarwin/api"; export async function POST(request: Request) { const body = await request.text(); try { const event = await Darwin.webhooks.verify(body, request.headers, process.env.DARWIN_WEBHOOK_SECRET!); if (event.type === "lead.captured" && event.data.object) { await addToCrm(event.data.object); } return new Response(null, { status: 204 }); } catch (error) { if (error instanceof WebhookVerificationError) return new Response(error.message, { status: 400 }); throw error; } } ``` In Express, `express.raw` must run on this route before any JSON body parser, so `req.body` is a Buffer. The Next.js handler works in the Node.js and Edge runtimes alike. ## Event payloads `event.data.object` is the contact, conversation, meeting, experiment or knowledge source the event is about, as the REST API returns it, or `null` if it no longer exists. `lead.captured` also carries `data.conversation`; `knowledge.source_failed` carries `data.error`. After a privacy erasure an event's payload becomes `{ erased: true }`. Narrowing on `event.type` gives you the right type for `data.object`. Every type: [Events and payloads](/api-reference/webhooks/events). The test event sent by `webhookEndpoints.test()` has type `webhook.test`. ## Test locally To exercise your receiver locally, `Darwin.webhooks.sign(body, secret)` returns the headers Darwin would send. ## Secret rotation During a secret rotation (`webhookEndpoints.rotateSecret`), deliveries carry a signature for each secret; `verify` accepts the delivery if any of them matches the secret you pass.