Skip to main content

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

A 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

A 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

A 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

The 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

Surface the connect-account screen — the stats/read endpoints still return data; only mutation is gated.

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/audiences
Creates a Meta Custom Audience on the connected ad account, optionally linked to a CDP segment so its membership can be synced later. Pass name, 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
Response
The persisted record carries the Meta-assigned 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
Returns the click-to-chat campaigns on the connected Meta ad account as a cursor-paginated list. When no ad account is connected the list is empty rather than an error, so the dashboard renders cleanly before setup. Request
Response
Page forward with ?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}
Returns one campaign plus a flattened insights summary (impressions, reach, clicks, CTR, spend, CPC, and conversations started) for the details view. The insights block is always included — no query param is needed. Request
Response
All metric fields are flattened onto one object (not the one-element array Meta’s Graph returns), so the details read is a single top-level insights.

4. Read attribution rollups per channel

GET /api/v1/ads/ctwa/attribution
Rolls up Click-to-WhatsApp (and RCS) entry points captured on inbound messages by ad: entry volume, distinct first-touched contacts, first-touch attributed revenue from local WhatsApp catalog orders, and — when a Meta ad account is connected — ad-spend-overlaid ROAS/ROI per ad. window_days (default 30, max 365) sets the look-back for both sides of the ratio. Request
Response
When no ad account is connected the same shape comes back with 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/status
There are two status surfaces. GET /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
Response
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
Response
Before any account is linked the same endpoint answers 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
The attribution export in step 4 attributes local WhatsApp catalog revenue first-touch to the entry ad and overlays Meta ad spend for ROAS — the report the dashboard’s ads console renders.