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

# CDP sync run log: drill into failed CRM sync rows

> Diagnose CRM/reverse-ETL destination syncs from the run log — per-destination run history with record-delta breakdowns and freshness/SLA roll-ups, a failed-row drilldown bucketed by error class with retryability hints, and the retry loop per failure class.

# CDP sync run log

When a CRM or reverse-ETL destination stops landing data, the sync run log is
the first surface to open. It is a read-only, per-destination history of every
sync run — and a drilldown into the failed rows of any single run, grouped by
the cause of the failure.

Two endpoints back the surface:

* `GET /api/v1/cdp/sync-run-log/runs` — the run list plus a per-destination
  health summary.
* `GET /api/v1/cdp/sync-run-log/runs/:runId` — one run's failed rows, bucketed
  by error class.

This guide walks both, shows how to read the error buckets, and closes with a
production checklist. For the destinations themselves, read the
[reverse ETL and warehouse exports operator walkthrough](/guides/cdp-reverse-etl-and-warehouse-exports).

## 1. What the run log is

Every CRM/reverse-ETL destination run — Salesforce, HubSpot, or a warehouse you
sync into — appends one entry to the run history. Each entry carries:

* **Identity** — `id`, `destination`, the object type and the segment the run
  synced with, plus the run's start/finish timestamps.
* **Status** — `ok`, `partial`, `failed`, or `skipped`.
* **Record-delta breakdown** — `attempted` (records that produced an upsert
  candidate), `delivered` (records in successfully dispatched batches),
  `rejected` (rows dropped before dispatch because the upsert identifier was
  missing or empty), and `failed` (records in batches whose dispatch failed).
* **Error classes** — the distinct failure classes present in the run, so the
  log line already tells you whether a partial run was a rate limit or a broken
  field map.

On top of the list, the summary rolls each destination up into a health row:
the last run and last success timestamps, the total run count, and two flags —
`failing` (the most recent run ended failed or partial) and `overdue`.

### The freshness SLA

A destination is `overdue` when its last **successful** run is older than
26 hours — or when it has runs but has never succeeded. The window leaves a
full day of slack for a nightly cadence before flagging, so an on-demand-only
destination is not spuriously alarmed unless it is also failing. Watch
`overdue` together with `failing`: a destination that is overdue *and* failing
is down; a destination that is merely overdue is a scheduler that stopped
running.

## 2. Prerequisites

* An **owner**, **admin**, or **developer** role on the organization — the log
  is an operator surface and rejects member roles.
* An API key with that role, or a dashboard session with it.
* At least one CRM/reverse-ETL destination configured and run — the endpoints
  return an empty list when no destination has ever synced.

Both endpoints are read-only and rate-limited to 60 requests per minute, which
suits dashboards and polling loops; it is not a bulk-export path.

## 3. Reading the run list

`GET /api/v1/cdp/sync-run-log/runs` returns the recent runs (newest first),
plus the per-destination summary. All filters are optional:

* `destination` — `salesforce`, `hubspot`, or another destination id.
* `status` — `ok`, `partial`, `failed`, or `skipped`.
* `limit` — 1–200, default 50.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/cdp/sync-run-log/runs?destination=salesforce&status=partial&limit=20" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Response shape (trimmed):

```json theme={null}
{
  "data": {
    "runs": [
      {
        "id": "run_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
        "ts": "2026-09-03T04:00:00.000Z",
        "destination": "salesforce",
        "object_type": "Contact",
        "status": "partial",
        "rows": {
          "attempted": 1200,
          "delivered": 1188,
          "rejected": 9,
          "failed": 3
        },
        "error_classes": ["validation", "rate_limit"],
        "has_failures": true
      }
    ],
    "summary": {
      "total_runs": 1,
      "destinations": [
        {
          "destination": "salesforce",
          "runs": 1,
          "last_run_at": "2026-09-03T04:00:00.000Z",
          "last_success_at": "2026-09-02T04:00:00.000Z",
          "last_status": "partial",
          "failing": true,
          "overdue": false,
          "freshness_ms": 86400000
        }
      ],
      "failing": 1,
      "overdue": 0,
      "needs_attention": true
    }
  }
}
```

Read the summary first. `needs_attention: true` tells you any destination is
failing or overdue before you look at a single run; the per-destination row
then points you at the destination to filter on. Only then open the run list
for the concrete run ids.

## 4. Drilling into a run

`GET /api/v1/cdp/sync-run-log/runs/:runId` returns one run with its failed rows
grouped into **buckets by error class**. Each bucket carries:

* `class` — the stable taxonomy value (see the next section).
* `rows` — records attributed to this class.
* `occurrences` — distinct failure events (failed batches or rejected rows).
* `retryable` — whether a bare retry could clear it without operator action.
* `samples` — a bounded sample (up to five, each trimmed) of the transport
  error messages in this bucket.

An unknown run id returns `404` with code `SYNC_RUN_NOT_FOUND`.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/cdp/sync-run-log/runs/run_01J8Z9K3P4Q5R6S7T8U9V0W1X2" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{
  "data": {
    "id": "run_01J8Z9K3P4Q5R6S7T8U9V0W1X2",
    "destination": "salesforce",
    "status": "partial",
    "total_failed_rows": 12,
    "buckets": [
      {
        "class": "validation",
        "retryable": false,
        "rows": 9,
        "occurrences": 9,
        "samples": [
          "Row dropped: missing or empty upsert identifier value"
        ]
      },
      {
        "class": "rate_limit",
        "retryable": true,
        "rows": 3,
        "occurrences": 1,
        "samples": ["Salesforce API returned 429: too many requests"]
      }
    ]
  }
}
```

The bucket list is the drilldown: 9 rows never left the platform (a missing
upsert key — a mapping problem), and 3 rows were throttled by Salesforce (a
cadence problem). Two different fixes; the log tells you which is which
without reading raw provider logs.

## 5. Diagnosing the failure classes

Every failure lands in one of nine classes, and the class decides the retry
loop. Retryable classes may clear on their own; the rest need an operator
change first — retrying them just fails again.

| Class        | Meaning                                             | Retryable        | Where it goes on the retry loop                                |
| ------------ | --------------------------------------------------- | ---------------- | -------------------------------------------------------------- |
| `auth`       | Expired or invalid credential at the destination    | No               | Reconnect the destination with fresh credentials, then re-sync |
| `permission` | Authenticated but missing a scope or role           | No               | Grant the missing permission at the destination, then re-sync  |
| `rate_limit` | Throttled by the destination                        | Yes              | Back off; slow the cadence if it recurs                        |
| `network`    | Transport failure reaching the destination          | Yes              | Retry; check the destination's reachability if persistent      |
| `timeout`    | No response in time                                 | Yes              | Retry; watch for destination slowness                          |
| `validation` | Payload, field map, or record rejected as invalid   | No               | Fix the field mapping or the record, then re-sync              |
| `not_found`  | Target object, field, or integration does not exist | No               | Correct the object/field reference, then re-sync               |
| `server`     | Destination returned a 5xx — its side, not yours    | Yes              | Retry; escalate to the destination if sustained                |
| `unknown`    | Could not be classified                             | Yes (retry once) | Inspect the bucket sample, then treat it as its apparent class |

Two of these have a fixed shape worth memorizing:

* **`validation` buckets from rejected rows** always carry the sample
  "Row dropped: missing or empty upsert identifier value" — the row left the
  segment but had no value in the mapped upsert-identifier field. Fix the
  mapping (or backfill the field at the source) before the next run.
* **Legacy runs** (from before the enriched history wrote per-batch failures)
  attribute the run's failed rows to the class of the single last-recorded
  error. The buckets still work — they just have coarser samples.

## 6. Worked example: a failed Salesforce auth run

A nightly Salesforce Contact sync stops landing. The summary row shows
`failing: true`, so filter the list to that destination:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/cdp/sync-run-log/runs?destination=salesforce&status=failed" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

The newest run comes back with `"error_classes": ["auth"]` and
`"rows": { "attempted": 4120, "delivered": 0, "rejected": 0, "failed": 4120 }`.
Drill it:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/cdp/sync-run-log/runs/run_01J9BK4N8PQ5T6V8W2Z1X3Y4AB" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

```json theme={null}
{
  "data": {
    "buckets": [
      {
        "class": "auth",
        "retryable": false,
        "rows": 4120,
        "occurrences": 4,
        "samples": ["Salesforce responded with error 401: INVALID_SESSION_ID"]
      }
    ]
  }
}
```

The bucket says `retryable: false`, so re-running changes nothing. Reconnect
the destination from the CDP integrations page with a fresh Salesforce
credential, then trigger a run and confirm:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/cdp/sync-run-log/runs?destination=salesforce&limit=1" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Expect `status: "ok"` and `delivered` equal to `attempted`. The summary row
flips back to `failing: false` and `overdue: false` once a success lands.

## 7. Security: no secret material in the log

The buckets contain no payload chain. Samples carry transport error copy only —
never contact records, never field values, never credentials — and each sample
is truncated so the detail payload stays bounded. The endpoints read the run
history projection and nothing else, so the log is safe to share with an audit
channel while still naming the cause of the failure. Credentials for the
destinations themselves remain write-only on the config surfaces; the run log
never touches them.

## 8. Production checklist

* Check the summary's `needs_attention` flag before scanning individual runs.
* Treat `failing && overdue` as *down*; treat `overdue` alone as a stopped
  schedule.
* Read the bucket class before retrying: retry only `rate_limit`, `network`,
  `timeout`, `server`, and (once) `unknown`.
* Fix `auth`, `permission`, `validation`, and `not_found` at the destination,
  the mapping, or the record — then re-run.
* On `rate_limit`, slow the destination's cadence instead of hammering retries.
* Keep polling within the 60-requests-per-minute per-endpoint limit.
* Route the destination's config edits through the CDP integrations page; the
  run log is read-only by design.

## Related guides

* [CDP reverse ETL and warehouse exports](/guides/cdp-reverse-etl-and-warehouse-exports)
  — the operator walkthrough for the destinations themselves.
* [Reverse ETL to Snowflake and BigQuery](/guides/reverse-etl-warehouse-exports)
  — the per-destination setup recipes.
* [API reference — CDP endpoints](/api-reference/endpoints/cdp) — the
  per-operation shapes for every endpoint named above.
