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

# Sync Leads from CRM

> Integrate your CRM with Naturalead using the lead sync API for idempotent upserts and bulk operations.

## Overview

The lead sync API lets you push leads from your CRM (HubSpot, Salesforce, Pipedrive, or any custom system) into Naturalead. It supports idempotent upserts using an external `lead_id`, so you can run periodic syncs without creating duplicates.

<CardGroup cols={2}>
  <Card title="Idempotent Upserts" icon="arrows-rotate">
    Use your CRM's lead ID as the external identifier. Re-syncing the same lead updates it instead of creating a duplicate.
  </Card>

  <Card title="Bulk Operations" icon="layer-group">
    Sync up to 100 leads per request. The API handles partial failures gracefully.
  </Card>
</CardGroup>

## Authentication

Authenticate sync requests with an API key.

```bash theme={null}
curl -H "X-API-Key: nl_live_your_api_key_here" ...
```

<Info>
  Sync requests require an API key with the `leads:sync_create` scope. Create one in **Settings > API Keys** in the dashboard.
</Info>

## Using External lead\_id for Idempotent Upserts

Every lead in the sync payload can include a `lead_id` field that maps to your CRM's unique identifier. When Naturalead receives a lead with a `lead_id` that already exists in your account, it updates the existing lead instead of creating a new one.

This makes your sync operations **idempotent** -- running the same sync twice produces the same result.

## Bulk Sync Example

Send an array of leads to the sync endpoint. Each lead should include a `lead_id` from your CRM.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.naturalead.ai/api/leads/sync \
    -H "X-API-Key: nl_live_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "source": "hubspot",
      "leads": [
        {
          "lead_id": "hs_12345",
          "name": "Alice Johnson",
          "phone": "+14155551234",
          "email": "alice@example.com",
          "customFields": {
            "company": "Acme Corp",
            "deal_stage": "discovery",
            "deal_value": 50000
          },
          "tags": ["enterprise", "inbound"]
        },
        {
          "lead_id": "hs_12346",
          "name": "Bob Smith",
          "phone": "+14155555678",
          "email": "bob@example.com",
          "customFields": {
            "company": "Globex Inc",
            "deal_stage": "proposal",
            "deal_value": 25000
          },
          "tags": ["smb", "outbound"]
        }
      ]
    }'
  ```

  ```python Python theme={null}
  import requests

  url = "https://api.naturalead.ai/api/leads/sync"
  headers = {
      "X-API-Key": "nl_live_your_api_key_here",
      "Content-Type": "application/json",
  }
  payload = {
      "source": "hubspot",
      "leads": [
          {
              "lead_id": "hs_12345",
              "name": "Alice Johnson",
              "phone": "+14155551234",
              "email": "alice@example.com",
              "customFields": {
                  "company": "Acme Corp",
                  "deal_stage": "discovery",
                  "deal_value": 50000,
              },
              "tags": ["enterprise", "inbound"],
          },
          {
              "lead_id": "hs_12346",
              "name": "Bob Smith",
              "phone": "+14155555678",
              "email": "bob@example.com",
              "customFields": {
                  "company": "Globex Inc",
                  "deal_stage": "proposal",
                  "deal_value": 25000,
              },
              "tags": ["smb", "outbound"],
          },
      ],
  }

  response = requests.post(url, headers=headers, json=payload)
  result = response.json()

  print(f"Created: {result['created']}")
  print(f"Updated: {result['updated']}")
  print(f"Failed: {result['failed']}")
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.naturalead.ai/api/leads/sync", {
    method: "POST",
    headers: {
      "X-API-Key": "nl_live_your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      source: "hubspot",
      leads: [
        {
          lead_id: "hs_12345",
          name: "Alice Johnson",
          phone: "+14155551234",
          email: "alice@example.com",
          customFields: {
            company: "Acme Corp",
            deal_stage: "discovery",
            deal_value: 50000,
          },
          tags: ["enterprise", "inbound"],
        },
        {
          lead_id: "hs_12346",
          name: "Bob Smith",
          phone: "+14155555678",
          email: "bob@example.com",
          customFields: {
            company: "Globex Inc",
            deal_stage: "proposal",
            deal_value: 25000,
          },
          tags: ["smb", "outbound"],
        },
      ],
    }),
  });

  const result = await response.json();
  console.log("Created:", result.created);
  console.log("Updated:", result.updated);
  console.log("Failed:", result.failed);
  ```
</CodeGroup>

## Syncing Incomplete Records

Only `lead_id` and `phone` are required. CRM records that are missing a name, an email address, or both are accepted and synced as-is.

| Field     | Required | Behavior when missing                                                                                                                                 |
| --------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lead_id` | Yes      | The lead is rejected.                                                                                                                                 |
| `phone`   | Yes      | The lead is rejected.                                                                                                                                 |
| `name`    | No       | The lead is stored without a name. The agent opens the conversation without addressing the lead by name, and the dashboard shows the lead as unnamed. |
| `email`   | No       | The lead is reachable on WhatsApp and Telegram. Email campaigns skip it.                                                                              |
| `source`  | No       | Defaults to `sync`.                                                                                                                                   |

On a re-sync, a field you omit keeps whatever value is already stored, so a partial payload never overwrites data with blanks. To clear a stored name or email, send it explicitly as `""` or `null`.

```json theme={null}
{
  "leads": [
    {
      "lead_id": "hs_12348",
      "phone": "+14155559876"
    },
    {
      "lead_id": "hs_12349",
      "phone": "+14155551111",
      "email": null
    }
  ]
}
```

## Handling Partial Failures

When syncing multiple leads, some may succeed while others fail validation. The API does **not** roll back successful operations when others fail. Instead, it returns a detailed breakdown.

```json theme={null}
{
  "created": 1,
  "updated": 1,
  "failed": 1,
  "errors": [
    {
      "index": 2,
      "lead_id": "hs_12347",
      "error": "Invalid phone number format"
    }
  ]
}
```

<Warning>
  Always check the `errors` array in the response. A `200` status code does not mean every lead was synced successfully -- it means the request was processed, and the response contains the outcome for each lead.
</Warning>

**Recommended error handling strategy:**

1. Log all entries from the `errors` array with their `lead_id` for investigation.
2. Fix the invalid data in your CRM.
3. Re-sync only the failed leads in the next run.

## Bulk Delete for Synced Leads

To remove leads that were deleted in your CRM, use the bulk delete endpoint with the external `lead_id` values.

<CodeGroup>
  ```bash curl theme={null}
  curl -X DELETE https://api.naturalead.ai/api/leads/sync \
    -H "X-API-Key: nl_live_your_api_key_here" \
    -H "Content-Type: application/json" \
    -d '{
      "source": "hubspot",
      "lead_ids": ["hs_12345", "hs_12346"]
    }'
  ```

  ```python Python theme={null}
  import requests

  url = "https://api.naturalead.ai/api/leads/sync"
  headers = {
      "X-API-Key": "nl_live_your_api_key_here",
      "Content-Type": "application/json",
  }
  payload = {
      "source": "hubspot",
      "lead_ids": ["hs_12345", "hs_12346"],
  }

  response = requests.delete(url, headers=headers, json=payload)
  result = response.json()
  print(f"Deleted: {result['deleted']}")
  ```

  ```javascript Node.js theme={null}
  const response = await fetch("https://api.naturalead.ai/api/leads/sync", {
    method: "DELETE",
    headers: {
      "X-API-Key": "nl_live_your_api_key_here",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      source: "hubspot",
      lead_ids: ["hs_12345", "hs_12346"],
    }),
  });

  const result = await response.json();
  console.log("Deleted:", result.deleted);
  ```
</CodeGroup>

## Rate Limit Considerations

The sync API has a dedicated rate limit to protect system stability.

| Limit                    | Value |
| ------------------------ | ----- |
| Sync requests per minute | 50    |
| Leads per sync request   | 100   |
| Maximum leads per minute | 5,000 |

<Warning>
  If you exceed the rate limit, the API returns a `429 Too Many Requests` response with a `Retry-After` header indicating how many seconds to wait before retrying.
</Warning>

For large initial imports (more than 5,000 leads), break the sync into batches:

```python theme={null}
import time
import requests

BATCH_SIZE = 100
DELAY_BETWEEN_BATCHES = 1.5  # seconds

def sync_leads_in_batches(all_leads):
    for i in range(0, len(all_leads), BATCH_SIZE):
        batch = all_leads[i:i + BATCH_SIZE]
        response = requests.post(
            "https://api.naturalead.ai/api/leads/sync",
            headers={
                "X-API-Key": "nl_live_your_api_key_here",
                "Content-Type": "application/json",
            },
            json={"source": "hubspot", "leads": batch},
        )

        result = response.json()
        print(f"Batch {i // BATCH_SIZE + 1}: "
              f"created={result['created']}, "
              f"updated={result['updated']}, "
              f"failed={result['failed']}")

        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 60))
            print(f"Rate limited. Waiting {retry_after}s...")
            time.sleep(retry_after)
        else:
            time.sleep(DELAY_BETWEEN_BATCHES)
```

## Best Practices for Periodic Sync

<Steps>
  <Step title="Use External lead_id Consistently">
    Always include the CRM's unique lead identifier as `lead_id`. This is the key that makes upserts idempotent. Never omit it, or you risk creating duplicates.
  </Step>

  <Step title="Sync Incrementally">
    Rather than syncing your entire CRM on every run, track the last sync timestamp and only send leads that were created or modified since then. This reduces API calls and processing time.
  </Step>

  <Step title="Handle Deletes Separately">
    After syncing new and updated leads, query your CRM for recently deleted records and send those to the bulk delete endpoint.
  </Step>

  <Step title="Schedule During Off-Peak Hours">
    If you are syncing large volumes, schedule your sync jobs during off-peak hours to avoid competing with real-time conversation traffic for rate limit capacity.
  </Step>

  <Step title="Monitor and Alert on Failures">
    Log every sync response and set up alerts when the `failed` count exceeds a threshold. Persistent failures often indicate data quality issues in the CRM that should be resolved at the source.
  </Step>
</Steps>

<Note>
  For real-time sync (pushing leads as they are created in your CRM), consider using your CRM's webhook or event system to trigger individual lead creation via `POST /api/leads` instead of the bulk sync endpoint.
</Note>
