The Only API docs

Async jobs

The 202-plus-poll pattern behind refresh, backfill and export.

Anything that has to walk a platform API takes longer than a request should. Those operations return 202 Accepted immediately and run in the background.

The pattern

Start the job

curl -X POST "$BASE/api/crm/$CRM/accounts/$OFUID/subscribers/refresh" \
  -H "X-API-Key: $KEY"

Returns 202 with the job or state payload.

Watch it — poll or stream

Poll the matching status route:

curl "$BASE/api/crm/$CRM/accounts/$OFUID/subscribers/refresh/status" \
  -H "X-API-Key: $KEY"

The /refresh/status routes report cache freshness, not job progress — they return {success, cache: {…}} with no status or phase. To see whether a job is still running, use GET /refresh/active, or subscribe to server-sent events and watch refresh.progress / refresh.complete. The dashboard uses SSE.

Read the result

The job writes into the cache, so the result is just the corresponding /cached read — see Cached reads. Exports instead produce a downloadable ZIP.

The jobs

StartFreshness checkWhat it does
POST /accounts/{of_user_id}/subscribers/refresh…/subscribers/refresh/statusDelta-sync the subscriber cache
POST /accounts/{of_user_id}/transactions/refresh…/transactions/refresh/statusDelta-sync the transaction cache
POST /accounts/{of_user_id}/campaigns/refresh…/campaigns/refresh/statusDelta-sync campaigns and claimers
POST /accounts/{of_user_id}/backfillvia /refresh/activeBackfill historical data
POST /accounts/{of_user_id}/exports…/exports/{job_id}Build a data export ZIP — see Data exports

Two panel-wide helpers:

GET  /refresh/active                              # every running refresh job in the panel
POST /accounts/{of_user_id}/refresh/{kind}/clear  # clear a stuck job's state

One at a time, per account per kind

Starting a job while one of the same kind is already running is not an error. You get 202 with already_running: true and the in-flight job — state for refresh and backfill, job for exports.

{ "success": true, "already_running": true, "state": { "…": "…" } }

Treat it as success. Retrying on it just spins.

A stale job is superseded. The two clocks differ, and it matters: a refresh is abandoned after 10 minutes with no recorded progress — a healthy long job keeps resetting that clock, so it never looks stale — while an export is abandoned 30 minutes after it started, regardless of progress. The stale job is marked failed and yours starts fresh, so a crashed worker never blocks you permanently.

Exports behave the same way; only the payload key differs:

{ "success": true, "already_running": true, "job": { "job_id": "f01152d3…", "status": "running" } }

Design for at-most-once, not exactly-once

Treat already_running: true as success — something is already doing the work you wanted. No refresh or export route returns 409 for this; the 409s that exist are for a different class of mistake, like downloading an export before it has finished.

Don't poll tightly

Refresh jobs finish in seconds to minutes depending on account size; exports with messages and media can take many minutes. Poll every few seconds at most, or use SSE and stop polling entirely.

Status polls are free — they never touch the platform and carry no monthly quota cost. They do count against the per-minute rate limit, so do not spin on them.

On this page