
# Syncing your audiences

Send an existing ContactLevel audience to a connected destination, then check its progress. You can use the same connection ID to start the sync and check its status.

## Before you start

You need:

- Python 3.10+, Node.js 22+, or cURL, depending on your chosen example.
- A ContactLevel API key with public API and audience sync access.
- An audience containing the contacts you want to sync.
- A destination connected and set up in ContactLevel, such as Facebook or Google Ads.

The API key, audience, and connection must belong to the same organization. The API gets your organization from the key; you do not send an organization ID.

## 1. Copy and paste the setup

Choose **Python**, **JavaScript**, or **cURL** using the tabs. Switching languages updates all examples on this page.

Check your tools. Python needs `requests`; JavaScript uses Node.js's built-in `fetch`.

<CodeTabs syncKey="audience-sync-language">

```bash title="Python"
python -m pip install requests
```

```bash title="JavaScript"
node --version  # Use Node.js 22 or later; no packages needed.
```

```bash title="cURL"
curl --version
```

</CodeTabs>

For Python, paste the setup into `sync_audiences.py`. For JavaScript, use `sync_audiences.mjs`. For cURL, paste it directly into a Bash or zsh terminal.

Replace `YOUR_API_KEY` and `YOUR_AUDIENCE_ID`. Use your API key starting with `api_` and the audience's UUID. Keep your filled-in setup private.

<CodeTabs syncKey="audience-sync-language">

```python title="Python"
"""Audience sync walkthrough. Install requests, then run python sync_audiences.py."""

import json
from typing import Any

import requests

BASE_URL = "https://a.contactlevel.com"
API_KEY = "YOUR_API_KEY"
AUDIENCE_ID = "YOUR_AUDIENCE_ID"


def _request(
    method: str, path: str, payload: dict[str, Any] | None = None
) -> dict[str, Any]:
    response = requests.request(
        method,
        f"{BASE_URL}{path}",
        headers={"X-Api-Key": f"Bearer {API_KEY}"},
        json=payload,
        timeout=60,
    )
    print(f"\n{method} {path} -> HTTP {response.status_code}")
    try:
        result = response.json()
    except ValueError:
        print(response.text)
        response.raise_for_status()
        raise
    print(json.dumps(result, indent=2))
    response.raise_for_status()
    return result


def find_audiences() -> dict[str, Any]:
    return _request("POST", "/api/audiences/search", {"page": 1, "page_size": 25})


def fetch_audience() -> dict[str, Any]:
    return _request("GET", f"/api/audiences/{AUDIENCE_ID}")


def check_connections() -> dict[str, Any]:
    return _request("POST", "/api/connections/search", {})


def run_sync(connection_id: str) -> dict[str, Any]:
    return _request(
        "POST",
        "/api/audience-syncs/run",
        {"audience_id": AUDIENCE_ID, "connection_ids": [connection_id]},
    )


def check_status(
    sync_id: str | None = None, *, connection_id: str | None = None
) -> dict[str, Any]:
    if sync_id and connection_id:
        raise ValueError("Use either sync_id or connection_id, not both")
    payload: dict[str, Any] = {"audience_id": AUDIENCE_ID}
    if sync_id:
        payload["sync_ids"] = [sync_id]
    if connection_id:
        payload["connection_ids"] = [connection_id]
    return _request("POST", "/api/audience-syncs/status", payload)


if __name__ == "__main__":
    # Leave ONE call uncommented, then run this file again for each step.
    check_connections()
    # find_audiences()
    # fetch_audience()
    # run_sync(connection_id="YOUR_CONNECTION_ID")
    # check_status(connection_id="YOUR_CONNECTION_ID")
    # check_status(sync_id="YOUR_SYNC_ID")
    # check_status()
```

```javascript title="JavaScript"
// Save as sync_audiences.mjs, then run: node sync_audiences.mjs
const BASE_URL = "https://a.contactlevel.com";
const API_KEY = "YOUR_API_KEY";
const AUDIENCE_ID = "YOUR_AUDIENCE_ID";

async function request(method, path, payload) {
  const response = await fetch(`${BASE_URL}${path}`, {
    method,
    headers: {
      "X-Api-Key": `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: payload === undefined ? undefined : JSON.stringify(payload),
    signal: AbortSignal.timeout(60000),
  });
  console.log(`\n${method} ${path} -> HTTP ${response.status}`);
  const text = await response.text();
  let result;
  try {
    result = JSON.parse(text);
  } catch {
    console.log(text);
    throw new Error(`Expected JSON; HTTP ${response.status}`);
  }
  console.log(JSON.stringify(result, null, 2));
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return result;
}

function find_audiences() {
  return request("POST", "/api/audiences/search", { page: 1, page_size: 25 });
}

function fetch_audience() {
  return request("GET", `/api/audiences/${AUDIENCE_ID}`);
}

function check_connections() {
  return request("POST", "/api/connections/search", {});
}

function run_sync(connection_id) {
  return request("POST", "/api/audience-syncs/run", {
    audience_id: AUDIENCE_ID, connection_ids: [connection_id],
  });
}

function check_status({ sync_id, connection_id } = {}) {
  if (sync_id && connection_id) {
    throw new Error("Use either sync_id or connection_id, not both");
  }
  const payload = { audience_id: AUDIENCE_ID };
  if (sync_id) payload.sync_ids = [sync_id];
  if (connection_id) payload.connection_ids = [connection_id];
  return request("POST", "/api/audience-syncs/status", payload);
}

// Leave ONE call uncommented, then run this file again for each step.
await check_connections();
// await find_audiences();
// await fetch_audience();
// await run_sync("YOUR_CONNECTION_ID");
// await check_status({ connection_id: "YOUR_CONNECTION_ID" });
// await check_status({ sync_id: "YOUR_SYNC_ID" });
// await check_status();
```

```bash title="cURL"
# Paste into a Bash or zsh terminal. Keep using the same terminal.
export BASE_URL="https://a.contactlevel.com"
export API_KEY="YOUR_API_KEY"
export AUDIENCE_ID="YOUR_AUDIENCE_ID"
# Fill these in when you reach the relevant step.
export CONNECTION_ID="YOUR_CONNECTION_ID"
export SYNC_ID="YOUR_SYNC_ID"
```

</CodeTabs>

For Python and JavaScript, leave **only the current step's call uncommented at the bottom of the file**, then run the file again for each step. For cURL, paste each step's command into the same terminal.

<CodeTabs syncKey="audience-sync-language">

```bash title="Python"
python sync_audiences.py
```

```bash title="JavaScript"
node sync_audiences.mjs
```

```bash title="cURL"
# Run each cURL command below in the terminal where you set the variables.
```

</CodeTabs>

Each example prints the HTTP status and response body.

## 2. Confirm your audience

If you need to find the audience ID, run:

<CodeTabs syncKey="audience-sync-language">

```python title="Python"
find_audiences()
```

```javascript title="JavaScript"
await find_audiences();
```

```bash title="cURL"
curl --silent --show-error --fail-with-body --max-time 60 "$BASE_URL/api/audiences/search" \
  -H "X-Api-Key: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"page\":1,\"page_size\":25}" \
  --write-out '\nHTTP %{http_code}\n'
```

</CodeTabs>

This calls `POST /api/audiences/search` and lists the first 25 audiences. Copy the relevant `audiences[].id` into `AUDIENCE_ID`. You can change `page` in the helper to see more results.

Then run:

<CodeTabs syncKey="audience-sync-language">

```python title="Python"
fetch_audience()
```

```javascript title="JavaScript"
await fetch_audience();
```

```bash title="cURL"
curl --silent --show-error --fail-with-body --max-time 60 "$BASE_URL/api/audiences/$AUDIENCE_ID" \
  -H "X-Api-Key: Bearer $API_KEY" \
  --write-out '\nHTTP %{http_code}\n'
```

</CodeTabs>

This calls `GET /api/audiences/{audience_id}`. Check the returned `audience.name` to confirm you selected the right audience.

If you have not created and populated an audience yet, do that in ContactLevel first. You can also use the [API reference](/api) to create an audience and add existing contacts. Creating an empty audience does not start a sync.

## 3. Choose a destination connection

<CodeTabs syncKey="audience-sync-language">

```python title="Python"
check_connections()
```

```javascript title="JavaScript"
await check_connections();
```

```bash title="cURL"
curl --silent --show-error --fail-with-body --max-time 60 "$BASE_URL/api/connections/search" \
  -H "X-Api-Key: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  --data "{}" \
  --write-out '\nHTTP %{http_code}\n'
```

</CodeTabs>

This calls `POST /api/connections/search`. A connection looks like this:

```json
{
  "connections": [
    {
      "connection_id": "00000000-0000-4000-8000-000000000002",
      "name": "Facebook Ads Sync",
      "platform": "facebook",
      "status": "healthy",
      "configured": true,
      "setup_complete": true,
      "ready": true,
      "blocking_reason": null
    }
  ]
}
```

Copy the `connection_id` for the destination you want. For cURL, replace `YOUR_CONNECTION_ID` in `export CONNECTION_ID="YOUR_CONNECTION_ID"` and run that assignment again. Choose a connection with `ready: true`. If it is false, `blocking_reason` explains what needs fixing in ContactLevel before you can sync.

These are example IDs; use the IDs from your own responses.

## 4. Start the sync

Replace `YOUR_CONNECTION_ID` with the ID from step 3:

<CodeTabs syncKey="audience-sync-language">

```python title="Python"
run_sync(connection_id="YOUR_CONNECTION_ID")
```

```javascript title="JavaScript"
await run_sync("YOUR_CONNECTION_ID");
```

```bash title="cURL"
curl --silent --show-error --fail-with-body --max-time 60 "$BASE_URL/api/audience-syncs/run" \
  -H "X-Api-Key: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"audience_id\":\"$AUDIENCE_ID\",\"connection_ids\":[\"$CONNECTION_ID\"]}" \
  --write-out '\nHTTP %{http_code}\n'
```

</CodeTabs>

This sends:

```http
POST /api/audience-syncs/run
```

```json
{
  "audience_id": "00000000-0000-4000-8000-000000000001",
  "connection_ids": ["00000000-0000-4000-8000-000000000002"]
}
```

This single request creates or reuses the destination sync, enables it if necessary, and starts it. No configuration step or sync ID is required. The `/api/audience-syncs/configure` endpoint is deprecated and remains available only for backward compatibility.

The request waits up to 25 seconds for startup, then returns HTTP `202`. Example:

```json
{
  "audience_id": "00000000-0000-4000-8000-000000000001",
  "results": [
    {
      "sync_id": "00000000-0000-4000-8000-000000000003",
      "connection_id": "00000000-0000-4000-8000-000000000002",
      "platform": "facebook",
      "disposition": "started",
      "sync_run_id": "00000000-0000-4000-8000-000000000004",
      "error_message": null
    }
  ]
}
```

**HTTP `202` does not mean the sync completed. Check each result's `disposition`:**

| Disposition | What to do |
| --- | --- |
| `started` | The run started or already finished. Check status next. |
| `already_running` | A run already exists and is active. Check its status. |
| `pending` | Startup is not confirmed yet. Check status before trying again. |
| `failed` | Read `error_message` and resolve the problem before retrying. |

Explicitly selected destinations are automatically enabled, including previously disabled ones. No separate configure call is needed. The connection must already be set up and ready; unresolved destination deletions still block a run.

Omit `connection_ids` or send `[]` to run existing enabled destinations only. This does not enable disabled destinations or add new ones.

## 5. Check progress

Use the same connection ID:

<CodeTabs syncKey="audience-sync-language">

```python title="Python"
check_status(connection_id="YOUR_CONNECTION_ID")
```

```javascript title="JavaScript"
await check_status({ connection_id: "YOUR_CONNECTION_ID" });
```

```bash title="cURL"
curl --silent --show-error --fail-with-body --max-time 60 "$BASE_URL/api/audience-syncs/status" \
  -H "X-Api-Key: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"audience_id\":\"$AUDIENCE_ID\",\"connection_ids\":[\"$CONNECTION_ID\"]}" \
  --write-out '\nHTTP %{http_code}\n'
```

</CodeTabs>

This calls `POST /api/audience-syncs/status` with `audience_id` and `connection_ids`, and returns the latest sync for that connection in this audience.

Example response:

```json
{
  "audience_id": "00000000-0000-4000-8000-000000000001",
  "syncs": [
    {
      "sync_id": "00000000-0000-4000-8000-000000000003",
      "platform": "facebook",
      "status": "IN_PROGRESS",
      "total_records": 597,
      "records_processed": 200,
      "records_successful": 190,
      "records_failed": 10,
      "records_skipped": 0,
      "error_message": null
    }
  ]
}
```

Run the status check again after a few seconds. You do not need to call `run_sync()` again to refresh progress.

| Status | Meaning |
| --- | --- |
| `NOT_STARTED` or `not_ready` | A sync exists but has not started or is not ready. |
| `PENDING`, `SCHEDULED`, or `DELAYED` | The run is waiting. Check again later. |
| `IN_PROGRESS` | The run is still working. |
| `COMPLETED` | The run finished successfully. |
| `COMPLETED_WITH_ERRORS` | The run finished with some errors. Review the counts and any error message. |
| `FAILED` | The run failed. Read `error_message` before retrying. |

Counts describe the ContactLevel run, not the destination's matched audience size. Skipped records are reported separately from failed records. Before a run exists, status reports the saved startup state with zero counters.

### Other ways to check status

Check a particular sync using `sync_id` from the run response. For cURL, first set `SYNC_ID` to that value:

<CodeTabs syncKey="audience-sync-language">

```python title="Python"
check_status(sync_id="YOUR_SYNC_ID")
```

```javascript title="JavaScript"
await check_status({ sync_id: "YOUR_SYNC_ID" });
```

```bash title="cURL"
curl --silent --show-error --fail-with-body --max-time 60 "$BASE_URL/api/audience-syncs/status" \
  -H "X-Api-Key: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"audience_id\":\"$AUDIENCE_ID\",\"sync_ids\":[\"$SYNC_ID\"]}" \
  --write-out '\nHTTP %{http_code}\n'
```

</CodeTabs>

Check every destination configured for this audience:

<CodeTabs syncKey="audience-sync-language">

```python title="Python"
check_status()
```

```javascript title="JavaScript"
await check_status();
```

```bash title="cURL"
curl --silent --show-error --fail-with-body --max-time 60 "$BASE_URL/api/audience-syncs/status" \
  -H "X-Api-Key: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"audience_id\":\"$AUDIENCE_ID\"}" \
  --write-out '\nHTTP %{http_code}\n'
```

</CodeTabs>

Use either `connection_id` or `sync_id` in the helper, not both. The API accepts their plural forms as lists: `connection_ids` or `sync_ids`. Omitting both lists, or sending both empty, returns all destinations.

**Do not pass `sync_run_id` to a status filter.** It identifies an individual execution; `sync_id` identifies the audience's sync configuration.

## If a request fails

The examples print the response body when an HTTP request fails. Read its `detail` field. For HTTP `202`, inspect `results[].error_message` instead.

| Result | Next step |
| --- | --- |
| `401` | Check your API key. |
| `402` | Check the access or billing requirement in the response. |
| `404` on a connection status check | Confirm the audience and connection IDs. A connection has no sync to check until one has been created for this audience. |
| `409` | Read `detail`; the connection may be unavailable, incomplete, or the destination may be awaiting deletion. |
| `422` | Use valid UUIDs without duplicates. For status, populate only one ID list. |
| A timeout or `pending` startup | Check status before resubmitting; the run may still start. |
| `failed` with an internal service authentication or scope error | Contact support with the sync ID and error message. |

See the [API reference](/api) for the full request and response schemas.
