> For the complete documentation index, see [llms.txt](https://docs.themochi.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.themochi.app/common-tasks/lead-synchronization.md).

# Sync leads

To **sync** leads is to copy new or changed lead data into another system. Copy only the fields the destination needs. Use CLI browser approval for a one-time local export. Use an API key with only `leads:read` for a scheduled server sync. Never copy contact values or customer data into an AI prompt.

Before you start, confirm the exact paths, parameters, and response fields in the [generated OpenAPI contract](https://openapi.gitbook.com/o/bpgVa93BfrzaqXzuggv8/spec/mochi-api.json).

## Plan the sync

1. Check the current OpenAPI operations and response fields.
2. For the first import, request the first page without `updated_since`. Follow `next_cursor` until it is `null`.
3. Save each page by Mochi lead ID in a durable batch. An **upsert** means update an existing ID or insert a new one.
4. Save the opaque cursor only after that page is durable. Never decode or construct a cursor.
5. After a completed run, save its UTC time as a checkpoint. Pass that time as `updated_since` on the next run. Use a small time overlap and duplicate-safe upserts so retries or equal timestamps do not lose leads.
6. Request lead detail only when the list does not contain the field you need.
7. For failures, record the sanitized `X-Request-ID`, method, path, status, and UTC time. Never record the authorization header, cursor, response body, or contact data.

## Request leads changed since the last run

Set `MOCHI_UPDATED_SINCE` to the UTC checkpoint from the last completed run. Check the parameter names against OpenAPI before shipping, because OpenAPI is the current endpoint contract.

### cURL

```bash
curl --fail-with-body --silent --show-error --get \
  "${MOCHI_BASE_URL}/v1/leads/" \
  --header "Authorization: Bearer ${MOCHI_API_KEY}" \
  --header "Accept: application/json" \
  --data-urlencode "updated_since=${MOCHI_UPDATED_SINCE}" \
  --data-urlencode "page_size=100"
```

### Python

```python
import os

import requests

response = requests.get(
    f"{os.environ['MOCHI_BASE_URL']}/v1/leads/",
    headers={"Authorization": f"Bearer {os.environ['MOCHI_API_KEY']}"},
    params={"updated_since": os.environ["MOCHI_UPDATED_SINCE"], "page_size": 100},
    timeout=30,
)
response.raise_for_status()
page = response.json()
print({"count": len(page["data"]), "has_next_page": page["next_cursor"] is not None})
```

### Node.js / TypeScript

```typescript
const baseUrl = process.env.MOCHI_BASE_URL ?? "https://api.themochi.app";
const url = new URL("/v1/leads/", baseUrl);
url.searchParams.set("updated_since", process.env.MOCHI_UPDATED_SINCE!);
url.searchParams.set("page_size", "100");
const response = await fetch(url, {
  headers: { Authorization: `Bearer ${process.env.MOCHI_API_KEY}` },
  signal: AbortSignal.timeout(30_000),
});
if (!response.ok) throw new Error(`Mochi HTTP ${response.status}`);
const page = (await response.json()) as { data: Array<{ id: number }>; next_cursor: string | null };
console.log({ count: page.data.length, hasNextPage: page.next_cursor !== null });
```

### PHP

```php
<?php
$query = http_build_query([
    "updated_since" => getenv("MOCHI_UPDATED_SINCE"),
    "page_size" => 100,
]);
$handle = curl_init((getenv("MOCHI_BASE_URL") ?: "https://api.themochi.app") . "/v1/leads/?" . $query);
curl_setopt_array($handle, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("MOCHI_API_KEY")],
]);
$body = curl_exec($handle);
$status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
curl_close($handle);
if ($body === false || $status < 200 || $status >= 300) throw new RuntimeException("Mochi read failed");
$page = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
echo json_encode(["count" => count($page["data"]), "has_next_page" => $page["next_cursor"] !== null]);
```

## Pagination, retries, and verification

Pass `next_cursor` back unchanged and keep the same `updated_since` throughout one run. Retry only the read failures described in [Fix an API error](/troubleshooting-and-safety/errors.md) and [Handle rate limits](/troubleshooting-and-safety/rate-limits.md). Limit the number of attempts. Stop if Mochi rejects a cursor; silently restarting at page one can duplicate an entire export.

Verify the destination count, unique Mochi IDs, first/last checkpoint, page count, and a small set of non-sensitive fields. A read sync has no Mochi rollback; rollback the destination batch and retain the previous durable checkpoint if validation fails.
