> This page is for Unified.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.merge.dev/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.merge.dev/_mcp/server.

# Syncing Data

> Best practices for initial and incremental data syncing. Covers modified_after filtering, polling strategies, and avoiding redundant API calls.

---

## Overview

When syncing data with Merge, we recommend a combination of webhooks and polling.

#### Save your users' account token

Get the **account token** from the linking process for an embedded Merge Link. Learn more in our [embedded Merge Link guide](/get-started/link/#step-3).

You can also use the **Linked Account linked webhook** to get the **account token**. See the example payload below and learn how to configure Merge Webhooks in our [guide](/merge-unified/reading-data/webhooks/merge-webhooks).

To authenticate your API requests to Merge, save your users' account token in your database. You will need the account token to poll for data in **step 4**.

**`Example Linked Account linked webhook payload`**

```json {4, 21} Example Linked Account linked webhook payload
{
  "hook": {
    "id": "e8affe31-8ae0-4b37-8c50-d86303094dc4",
    "event": "LinkedAccount.linked",
    "target": "https://webhook.site/105606a8-cfa3-4415-bae0-954e25137f00"
  },
  "linked_account": {
    "id": "4ac10f37-c656-4e9a-89a1-1b04f9e9a343",
    "integration": "Ashby",
    "integration_slug": "ashby",
    "category": "ats",
    "end_user_origin_id": "12345678910",
    "end_user_organization_name": "Example Organization",
    "end_user_email_address": "jack.cavalier@merge.dev",
    "status": "COMPLETE",
    "webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/abcd1234defg5678",
    "is_duplicate": false,
    "account_type": "PRODUCTION"
  },
  "data": {
    "account_token": "{ACCOUNT-TOKEN}",
    "is_relink": false
  }
}
```

#### Sync data when Merge emits a sync notification webhook

We recommend using the **Linked Account synced webhooks** to manage sync activities at scale. Whenever you receive a sync notification webhook for a Linked Account, start pulling data and kick off the logic in **step 3**.

**Important fields:**

| Field               | Description                                                                                                                                                                                  |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hook.event`        | The event type that triggered the webhook. See our [webhooks guide](/merge-unified/reading-data/webhooks/merge-webhooks) for more information.                                               |
| `linked_account.id` | The ID of the associated Linked Account.                                                                                                                                                     |
| `data.sync_status`  | Handle edge cases when `last_sync_result` is `FAILED` or `PARTIALLY SYNCED`. See our Help Center article on [sync statuses](https://help.merge.dev/en/articles/8184193-merge-sync-statuses). |

**`Example Linked Account synced webhook payload`**

```json {4, 8, 23-37} Example Linked Account synced webhook payload
{
  "hook": {
    "id": "e8affe31-8ae0-4b37-8c50-d86303094dc4",
    "event": "LinkedAccount.sync_completed",
    "target": "https://webhook.site/105606a8-cfa3-4415-bae0-954e25137f00"
  },
  "linked_account": {
    "id": "4ac10f37-c656-4e9a-89a1-1b04f9e9a343",
    "integration": "Ashby",
    "integration_slug": "ashby",
    "category": "ats",
    "end_user_origin_id": "12345678910",
    "end_user_organization_name": "Example Organization",
    "end_user_email_address": "jack.cavalier@merge.dev",
    "status": "COMPLETE",
    "webhook_listener_url": "https://api.merge.dev/api/integrations/webhook-listener/abcd1234defg5678",
    "is_duplicate": false,
    "account_type": "PRODUCTION"
  },
  "data": {
    "is_initial_sync": true,
    "integration_name": "Ashby",
    "integration_id": "ashby",
    "sync_status": {
      "ats.Candidate": {
        "last_sync_finished": "2023-12-29T18:57:12Z",
        "last_sync_result": "PARTIALLY_SYNCED"
      },
      "ats.Application": {
        "last_sync_finished": "2023-12-29T18:59:25Z",
        "last_sync_result": "DONE"
      },
      "ats.Job": {
        "last_sync_finished": "2023-12-29T17:05:45Z",
        "last_sync_result": "FAILED"
      }
    }
  }
}
```

#### Create functions for efficiently syncing data

Store the timestamp of **when you last started pulling data** from Merge as `modified_after`. Use this timestamp in subsequent API requests to pull updates from Merge since your last sync.

Use the expand parameter to pull multiple models that are related to each other instead of making multiple pulls for related information.

**Query parameters:**

#### GET /\{endpoint}/?modified\_after=\{timestamp}

Only pull data that has been changed or created since your last sync.

For example, you can ask for `modified_after=2021-03-30T20:44:18`, and only pull items that are new or changed.

#### GET /\{endpoint}/?expand=\{common\_model\_name}

Pull related model information with a single API request.

For example, if you are querying for candidates and also want details about associated applications, you can `expand=applications`, and Merge will return the actual application objects instead of just the `application_id`.

**`Example code snippet`**

```python Example code snippet
from merge_hris_python import EmployeesApi, Configuration, ApiClient
from datetime import datetime, timedelta

def get_modified_employees(account_token, modified_after, expand):
    config = Configuration()
    api_client = ApiClient(configuration=config)
    employees_api = EmployeesApi(api_client=api_client)

    # Ensure datetime in ISO 8601 format
    modified_after = modified_after.isoformat()

    # Call GET /employees with "modified_after" and "expand" parameters
    response = employees_api.employees_list(account_token, modified_after=modified_after, expand=expand)

    return response

# Usage
account_token = "YOUR_ACCOUNT_TOKEN"
modified_after = datetime.now() - timedelta(days=7)  # Get employees modified in the last 7 days
expand = "manager,employments"  # Expand manager and employments objects
```

#### Sync periodically and poll using /sync-status endpoint

> **Info**
>
> **Make sure to implement polling and don't rely entirely on notification webhooks.**
>
> Webhooks can fail for a variety of reasons such as downtime or failed processing. Merge does attempt to redeliver multiple times using exponential backoff, but we still recommend calling your sync functions periodically every 24 hours.

Make a request to our **/sync-status** endpoint, which returns one syncing status per Common Model in the category that has a public endpoint. See [API reference](/merge-unified/hris/data-management/sync-status/list) to learn more.

* If status is `PARTIALLY SYNCED` or `DONE`, go ahead and retrieve data. If another sync has not started, since the last time you pulled data, there will not be new data.
* If status is `SYNCING`, continue pinging

Look up each model by its `model_id` instead of by position, and avoid waiting on a hard-coded list of models. Merge adds Common Models to a category over time, and a model with no public endpoint to query is left out of the response.

**`Example code snippet`**

```python Example code snippet
def is_sync_complete(merge_client, common_model_id):
    # Get sync statuses for all common models
    sync_statuses = merge_client.ats.sync_status.list()

    # Find the status of the specified model
    sync_status = next(
        (model for model in sync_statuses.results if model.model_id == common_model_id),
        None,
    )
    if not sync_status:
        raise ValueError(f"Invalid common model id: {common_model_id}")

    # Assess the current sync status
    if sync_status.status == "SYNCING":
        return False

    elif sync_status.status in ["FAILED", "DISABLED", "PAUSED"]:
        raise RuntimeError(f"Sync failed with status: {sync_status.status}")

    elif sync_status.status in ["DONE", "PARTIALLY_SYNCED"]:
        return True

    return False


def sync_candidates_data(api_key, account_token, last_synced_from_merge):
    CANDIDATE_COMMON_MODEL_ID = "ats.Candidate"
    merge_client = Merge(api_key=api_key, account_token=account_token)

    # poll for complete sync
    while not is_sync_complete(merge_client, CANDIDATE_COMMON_MODEL_ID):
        time.sleep(30)

    # Retrieve data from the candidates endpoint
    updated_last_synced_from_merge = timestamp.now()
    data = merge_client.ats.candidates.list(modified_after=last_synced_from_merge)

    return data, updated_last_synced_from_merge
```