Skip to main content

Anomaly examples — worked API samples over the anomaly-insight ledger

The anomaly-insight ledger is the platform’s append-only record of what its own detectors have raised against your account traffic — spend spikes, toll-fraud blocks, SIM-swap flags, webhook-delivery anomalies. This page is the API-samples counterpart of Anomaly detection — reading usage and delivery anomalies, which walks the same data through the dashboard. Everything below is real: the three endpoints, the exact envelope shape, and the query parameters are the ones the platform serves today. All three endpoints are tenant-scoped GETs under /api/v1/insights and return the standard Orbit envelope: data carries the payload, meta carries the request id. Reads are idempotent and safe to poll; there is no object to create and no decision to post from your side — triage (acknowledge / dismiss / resolve) happens on the Insights → overview Fraud alerts card, not over these endpoints.

Sample 1 — list anomalies by scope (the issues listing)

The by-scope endpoint answers “is anything wrong right now?” with one aggregate row per forensics scope (wallet, webhook). This is the listing your poller should hit first, because it is the cheapest way to decide whether a deeper page through /history is even needed.
Read the envelope like this:
  • scopes always carries wallet and webhook aggregate rows plus the all-scopes total, regardless of your scope= filter; the filter only narrows which rows fed the aggregate.
  • open_count is the decision input — anything above zero means a detector raised something nobody has triaged yet.
  • high_or_critical_count tells you the entry is a same-day item, not a queue-for-review one (severity ranks low → medium → high → critical).
  • truncated is null on a healthy window. When a busy window runs into the bounded row scan, it carries { "row_scan_bound": N } and you should page through /history instead of trusting the totals for that window — the platform surfaces the bound rather than silently undercounting.
The accepted parameters are the same on all three endpoints: days (default 30, maximum 365), an explicit from/to ISO datetime pair (which overrules days), and scope=wallet|webhook.

Sample 2 — the trendline (decision pass over day buckets)

The timeseries endpoint is the “blip or pattern?” pass. Day buckets are zero-filled across the window — a day with no detections returns 0, never a missing day — so a consumer can plot it without gap handling and a poller can compare “today” against a clean baseline of zeros.
One tall day against a flat baseline is a burst (a campaign, an attack wave); a rising staircase is a drift — misconfiguration or sustained abuse. That distinction is the decision this sample exists to pass back upstream: the by-scope call says something is open, and the trendline says whether it is new or sustained.

Sample 3 — the history rows (the full envelope split by webhook scope)

The history endpoint returns the raw ledger rows, newest first, paged at up to 100 entries per request. Filter it to scope=webhook and you get the inbound-side family — webhook-delivery and phone-number anomalies — which is exactly the webhook shape your receiver-side tooling cares about:
Every row is scrub-on-read: details carries only tenant-safe hints (an endpoint id, a destination mask, a reason code) — provider-internal payloads never survive the read path. channel is null on webhook-scope rows and carries the spend channel (sms, whatsapp, email, rcs, voice, agents, or all) on wallet-scope rows. The row status moves open → acknowledged → resolved as your team works it on the Insights → overview card, so polling /history also tells you what is already being handled. The full triage workflow is covered in Debug inbound carrier webhooks; this page covers the anomaly ledger, not the per-webhook debug records, so nothing here duplicates that guide.

The consistent 200 / 422 envelope

All three endpoints share one envelope contract:
  • 200data carries the payload shown above, meta carries the request_id. An empty window is a valid answer: empty items, zeroed buckets, zeroed open_count — an empty ledger is the healthy account state, not an error.
  • 422 — the same standard Orbit validation envelope every endpoint returns on a malformed query (days above 365, a scope outside wallet|webhook, an unparseable from/to). Fix the parameter and re-send; nothing about the response shape changes.

Sample 4 — Node SDK (orbit.request) — the trigger-or-pass decision call

The Node SDK exposes the platform-generic orbit.request method, which carries any of these GETs with the retry, idempotency, and timeout posture the SDK applies to everything. The snippet below is the pass this page actually ships: poll the by-scope aggregate, pass on a quiet window, and only page through /history when something is genuinely open.
The pass deliberately reads truncated before trusting the totals — that is the one failure shape this API can otherwise hide, and the guard above is the correct handling for it. orbit.request applies the SDK’s retry posture on transient 5xxs, so a poller loop around anomalyDecisionPass is safe to run on a fixed cadence.

See also