Skip to main content

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 via record, or via the /g/:goalId/:contactId.gif tracking pixel for browser conversion attribution.

Examples

Every response lands in the standard envelope: the payload under data, 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).
Response 200data.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:
If no messages match the window, you still get a 200 with zeroed totals and an empty time_series.

Per-channel breakdown — GET /analytics/messages/by-channel

Same date window params as above, no group_by — one rollup row per channel. WhatsApp tenants with multiple connections can pass phone_number_id to scope the breakdown to one sending number.
Response 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.
Response 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).
Response 201:
2 — Record a conversion. The body accepts 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.
Response 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:
3 — Read the goal stats. window_days (1–365, default 30) bounds which conversions count.
Response 200 — top-10 buckets per attribution axis:
Tracking pixels for browser conversions. Goals of type 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 require owner 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:
Response 201next_send_at is the upcoming cadence tick, not an internal trigger value; a PATCH that changes frequency recomputes it from the new cadence:
Trigger an out-of-band run. Queues the report for immediate delivery without waiting for the next tick; the dashboard polls last_sent_at to show progress.
Response 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:
The tile shows 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 (or start_date/end_date) must equal the dashboard’s selected range.
  • Grouping mismatch — same-window tiles can differ when one side groups by campaign and the other by queue.
  • 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.
A gap that persists across matching windows and a cache-age wait is worth a support ticket — include the 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:
The cursor variant (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