Analytics API
Aggregated analytics over message traffic, deliverability, costs, and goal conversions. All read endpoints are served from a database read-replica, so they’re cheap to call but lag the primary by 1-3 seconds. Base path:/api/v1/analytics
Authentication: API key (X-API-Key) or session JWT.
Message analytics
Scheduled reports
Email-delivered analytics rollups on a schedule.Conversion goals
A goal is a tracked conversion (signup, purchase, trial start, etc.). Hits are recorded server-side viarecord, or via the /g/:goalId/:contactId.gif tracking pixel for browser conversion attribution.
Examples
Every response lands in the standard envelope: the payload underdata, and meta.request_id / meta.timestamp alongside. Examples below trim the meta block. The Node SDK (new Orbit({ apiKey })) isn’t published to a registry yet — call any endpoint through its generic orbit.request() helper until the typed resource catches up.
Message volume time series — GET /analytics/messages
Query params:start_date / end_date (YYYY-MM-DD or ISO datetime, max 365-day range), plus days as a rolling-window shortcut; optional channel, status, campaign_id, and group_by (hour | day | week | month, default day).
200 — data.totals covers the whole window; data.time_series carries one bucket per group_by period. period is the bucket’s UTC start and period_date is the tenant-local calendar date for that bucket:
200 with zeroed totals and an empty time_series.
Per-channel breakdown — GET /analytics/messages/by-channel
Same date window params as above, nogroup_by — one rollup row per channel. WhatsApp tenants with multiple connections can pass phone_number_id to scope the breakdown to one sending number.
200 — engagement counters (total_opened, total_clicked, total_complained, total_soft_bounced) are email-relevant and read 0 on channels that never stamp them:
Delivery / read / failure rates — GET /analytics/deliverability
channel is required (sms, whatsapp, rcs, email, voice, instagram, messenger, viber). window is 24h | 7d | 30d (default 7d), and groupBy overrides the bucket size (hour, default for 24h; day otherwise). Results are cached for 60 seconds, so plan for up to a minute of staleness on top of the read-replica lag.
200 — rates use terminal-status messages as the denominator so in-flight sends don’t deflate the success number; timezone names the zone the series buckets were aligned on (render the x-axis labels in that zone, not the browser’s):
Conversion attribution round trip — goals
A full attribution loop is four calls: create the goal, record conversions for it, then read the aggregated stats. Goal writes (POST / PATCH / DELETE, plus record) require the owner, admin, or developer role; reads are open to any authenticated member.
1 — Create the goal. type is one of pixel_fire, webhook_hit, manual, tag_added, custom_event; attribution_model is last_touch (default), first_touch, linear, or time_decay; lookback_days bounds how far back a touchpoint may claim credit (1–180, default 7).
201:
contact_id (optional), value_cents (overrides the goal’s default), and arbitrary metadata. Server-to-server callers outside a session must also pass Authorization: HMAC <contactId>:<sig>, where sig is the HMAC-SHA256 of the string <goal_id>:<contactId> computed with your API secret — an unsigned public request is rejected with 401.
201 — each conversion is written once per enabled attribution model the goal defines (usually one), and the ids below point back at the message / campaign / agent the goal’s model credited:
window_days (1–365, default 30) bounds which conversions count.
200 — top-10 buckets per attribution axis:
pixel_fire get a pixel_url hint back on the list/get responses. Embed a 1×1 image; the endpoint answers GET /g/:goalId/:contactId.gif?sig=<sig>&schema=<schema> with the pixel and no-cache headers (200 whether or not the conversion recorded, so the merchant page never shows a broken image). sig is the HMAC-SHA256 hex of <goal_id>:<contact_id>:<tenant_schema> using your API secret, and schema is your tenant schema name (both shown in the dashboard’s goal detail page). Signing the schema into the payload means a valid pixel from one workspace can’t mint a conversion in another.
Scheduled reports — create and trigger
Report writes requireowner or admin. type is one of messaging_volume, deliverability, top_contacts, spend, custom (custom runs the saved query in filters); frequency is daily | weekly | monthly, anchored to 09:00 in the report’s timezone (UTC when omitted). recipients takes 1–50 emails, as an array or a comma-separated string.
Create:
201 — next_send_at is the upcoming cadence tick, not an internal trigger value; a PATCH that changes frequency recomputes it from the new cadence:
last_sent_at to show progress.
200:
Reconcile API numbers against a dashboard tile
The dashboard reads the same aggregation endpoints you call here, so a KPI tile should match a curl response field for field when both cover the same window. Worked example: verify the Verified CTR tile on Insights → SMS click-throughs:totals.verified_click_rate from that response — 0.0604 in the SMS click-through parity sample renders as 6.04%. The same mapping holds everywhere: each tile is one field under data.totals, and the full set of buckets sits under data.rows.
If the tile and the API disagree, check three things before suspecting a bug:
- Window mismatch — the curl
window(orstart_date/end_date) must equal the dashboard’s selected range. - Grouping mismatch — same-window tiles can differ when one side groups by
campaignand the other byqueue. - Caching — aggregations cache for up to 60 seconds, and every read rides the read-replica (1–3 seconds of lag), so refresh both sides before comparing very recent sends.
meta.request_id from the curl response.
Paginate buckets with the Node SDK
Aggregation endpoints that list rows (/analytics/messages/by-country, /analytics/goals/{id}/conversions, /analytics/sms-click-through) are page-numbered rather than cursor-paginated — increment page until totalPages, or once you hold every row. The totals block already covers the whole window, so you can render the KPI header from page one and only page for the table:
meta.pagination.cursor / has_more) applies to record-list endpoints like GET /messages — walk it the same way, passing the cursor back until has_more is false. The full model is in the Pagination guide.
See also
- SMS click-through parity — per-campaign and per-queue CTR
- Search Analytics API — top queries, zero-result rates, and parse failures
- Webhooks → analytics events