Errors worth branching on
These five failures cover the ads stats sync (POST /api/v1/ads/sync), on every stats pull from the account linker. Each block below is a full{ error, meta } envelope as the API returns it, and the matrix at the bottom answers retry vs surface for the same five classes. For the platform-wide decision table these branches plug into, see the error handling guide.
401 — Unauthorized
401 on this page is the bearer key failing before the route ran — it never means the resource is wrong. Rotate the key or re-mint the scoped token; retrying the same request changes nothing.
403 — Forbidden
403 means the key authenticated but the operation is gated by scope — check the key’s scopes on the developer page; a 403 is never a data-not-found shape.
422 — Schema
422 means the payload did not match the request schema — branch on error.details.field and list campaigns first and resend with an existing campaign id instead of blind-retrying the same body.
429 — Rate
Retry-After header and error.details.retry_after are both set on every 429 — resend the SAME request after the lower of the two.
402 — Feature gate
60-second retry matrix
Worked samples — ads audiences, campaigns, attribution
Copy a request as written, substitute your own ids, and compare the response envelope — a success in the same{ data, meta } shape the numbered error sections spell out. Every operation below supports cURL, Node.js (TypeScript), Python, Go, Ruby, and PHP; for brevity each sample shows cURL and TypeScript — the two most-used — and a link to the per-operation block further down the page carries the other four languages.
The sequence at the end (create an audience → bind it to a campaign → read back insights → export attribution) is the path most workspaces take from segment to measured spend.
1. Create a custom audience
POST /api/v1/ads/audiencesname, an optional description, and an optional segment_id to bind the audience to a segment on creation; leave segment_id out and bind it later at sync time.
Request
audience_id plus the segment_id link you sent. last_synced_at and last_sync_count stay null until the first /sync run pushes membership; read the audience back any time with GET /api/v1/ads/audiences to see those two fields fill in. The audience is available for targeting on Meta as soon as it is created — there is no separate status poll on Orbit’s side.
2. List ad campaigns
GET /api/v1/ads/campaigns?cursor=1202103894124 until has_more is false. Budget strings come straight from Meta in the account’s currency — surface GET /api/v1/ads/status to read what that currency is.
3. Read one campaign with its insights
GET /api/v1/ads/campaigns/{id}insights.
4. Read attribution rollups per channel
GET /api/v1/ads/ctwa/attributionwindow_days (default 30, max 365) sets the look-back for both sides of the ratio.
Request
spend_available: false, roas: null, and cost_status: "no_spend" — entry counts and attributed revenue still roll up, only the spend overlay degrades.
5. Read audience status transitions
GET /api/v1/ads/audiences and GET /api/v1/ads/statusGET /api/v1/ads/audiences is the per-audience view — the sync metadata on each stored audience tells you whether it has ever pushed membership and when. GET /api/v1/ads/status is the connection-level view the dashboard polls — whether a Meta ad account is linked, which ad_account_id, the currency, and whether a Conversions API dataset is set.
Request — per-audience sync state
last_synced_at: null marks an audience that has not yet synced — the empty state before the first POST .../sync. A filled pair means the last push completed and carried last_sync_count members.
Request — connection status
200 with connected: false, ad_account_id: null, and currency: null — a clean pre-setup state, not an error.
6. A 4-op sequence: audience → campaign → insights → attribution
The path from a CDP segment to a measured ROAS. Run the four operations in order; the id from step 1 feeds step 2, the campaign id from step 2 feeds step 3, and step 4 closes the loop.Sequence