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

# Sync leads to a CRM

> 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={null}
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`](https://docs.meetdarwin.ai/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`.


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