---
title: "List sync runs"
method: GET
path: "/v1/sync-runs"
tags: ["ERP sync"]
---

# List sync runs

`GET /v1/sync-runs`

## Request

- Base URL: `https://api.getfinero.com/functions/v1/api`
- URL: `https://api.getfinero.com/functions/v1/api/v1/sync-runs`
- Auth: HTTP bearer

Lists ERP sync runs, newest first by creation time. Keeps the newest five hundred finished runs per connection, plus any running; older runs are removed daily. Cursor-paginated: pass `limit` (1–100) and send pagination.next_cursor back as `cursor` until has_more is false. To learn HOW MANY rows match without walking every page, pass `include_total=true` once and read pagination.total_count.

Available to **admin and readonly** API keys (no side effects).

## Query parameters

- `limit` integer
- `cursor` string
- `order` 'asc' | 'desc'
- `include_total` boolean
- `connection_id` string, uuid
- `status` 'running' | 'success' | 'partial' | 'failed'
- `direction` 'pull' | 'push'

## Response `200`

Success

- object
  - `data` object[], required: Sync runs, newest first by creation time.
    - `id` string, uuid, required: Sync run id — poll this run until status is terminal.
    - `connection_id` string, uuid, required: The ERP connection that ran.
    - `direction` 'pull' | 'push', required: Sync direction. Every new run is `pull` — Finero no longer writes invoices back to an ERP. `push` remains in the enum because historical runs carry it.
    - `trigger` 'manual' | 'scheduled' | 'api', required: How the run started: `manual` (a person, in the Finero app), `scheduled` (the connection's own cadence), or `api` (POST /v1/erp-connections/{connectionId}/sync). Until 2026-08-11 API-started runs were recorded as `manual`; they are `api` now, because reporting a machine-started run as a human one misattributes it.
    - `status` 'running' | 'success' | 'partial' | 'failed', required: running is non-terminal; success/partial/failed are terminal. `partial` means the run ended with some invoices processed and some not: what WAS processed is applied and trustworthy. `error_summary` says why the rest was not — a failure on some items, or a pause at a per-run limit that the next sync resumes from; null means it only skipped invoices (`items_skipped`).
    - `started_at` string, date-time, required: Run start time (ISO 8601 UTC).
    - `finished_at` string, date-time, nullable, required: Run finish time (null while running) (ISO 8601 UTC).
    - `items_processed` integer, required: Invoices durably processed by this run, including correctness-preserving boundary rechecks. This is operational work, not the number of ERP changes; use `items_changed` for that. Judge completion by `status` and freshness by the connection's `last_successful_sync_at`.
    - `items_changed` integer, nullable, required: New invoices or invoices whose persisted ERP `source_updated_at` changed in this run. Zero is valid when positive `items_processed` only rechecks an inclusive cursor boundary. Null means the historical count is unknown.
    - `items_failed` integer, required: Invoices this run read but could not process.
    - `items_skipped` integer, required: Invoices NOT imported because the ERP sent them without a required value (named in `skipped_invoices`). Not a failure: the rest was imported, and the run is `partial` (`success` before 2026-10-02).
    - `skipped_invoices` object[], required: The first 20 skipped invoices, so they can be found and fixed in the ERP.
      - `external_invoice_id` string, required: The ERP's own id for the invoice.
      - `invoice_number` string, nullable, required: Null when the number is itself what is missing.
      - `missing` string[], required: Which values were missing. `invoice_status` counts only while the connection collects invoice status.
    - `error_summary` string, nullable, required: Categorical failure summary (never raw provider payloads).
  - `pagination` object, required
    - `next_cursor` string, nullable, required: Opaque cursor for the next page — pass as ?cursor=. Null when there are no further results.
    - `has_more` boolean, required: Whether another page exists.
    - `limit` integer, required: The page size that was applied.
    - `total_count` integer: Total rows matching the filters, ignoring paging. Present ONLY when the request passed ?include_total=true.

## Other responses

- `400`: `invalid_cursor` — The cursor is malformed, was issued for a different query, or (aging) the report changed. Restart without cursor, keeping the filters identical. `validation_failed` — The request failed schema validation. See error.details for field messages.
- `401`: `invalid_credentials` — The credential is malformed or does not match an active key. The response never reveals whether a key identifier exists. `missing_credentials` — No Authorization: Bearer header was sent. `revoked_credentials` — The presented key was revoked. Create a new key in Settings → API.
- `403`: `feature_unavailable` — The tenant's tier does not currently include API access. Feature rollout flags cannot grant this entitlement. `permission_denied` — The key's permission does not allow this operation (readonly keys cannot call admin-only operations).
- `404`: `not_found` — No such resource in YOUR tenant. Ids belonging to another tenant are indistinguishable from missing ones.
- `429`: `rate_limit_exceeded` — Too many requests. Honor the Retry-After header (seconds) before retrying — waiting that long is enough. The RateLimit-* fields on every response say how much headroom is left before this happens; pace against them instead of retrying into a refusal.
- `500`: `internal_error` — Unexpected server error. Safe to retry with the same Idempotency-Key.

---

[API](https://skmtc.dev/getfinero/apis/finero-public-api.md) · [All operations](https://skmtc.dev/getfinero/apis/finero-public-api/llms.txt) · [OpenAPI document](https://skmtc.dev/getfinero/apis/finero-public-api/revisions/3d456e222775?raw)
