Reports API

Run report definitions, list the template gallery, and manage saved reports, their history and dashboards

This page as Markdown

The reports API runs what the report builder runs and stores what it saves: saved reports, their revisions, and dashboards. Every path is scoped to an account, and an account can only reach its own records.

Plan. Every route needs the Custom reports plan feature (403 FEATURE_NOT_IN_PLAN otherwise). After a downgrade, what you already saved can still be listed, read (with its revisions) and deleted: GET and DELETE on saved reports and dashboards keep working, while running, creating and changing them answer 403 FEATURE_NOT_IN_PLAN. Saving more reports or dashboards than the plan allows answers 409 ENTITLEMENT_LIMIT_REACHED. Firewall dimensions and metrics (waf_action, waf_blocked_count, distinct:waf_rule, …), the firewall’s decision tags read as tags (tag:waf.action, attr:waf.rule:…, distinct:tag:waf.policy) and filters on them (waf_action == "block", tag:waf.matched == "1") also need the firewall in the plan (wafEnabled): without it, running a report with them through POST /reports/query, creating a report with them or changing a saved report’s definition to one with them, saving a new or changed dashboard filter on them, or adding a report to a dashboard whose filter reads them (or changing the definition of a report that is a tile of one: data.cause is then "dashboardFilter", and the message names those dashboards and what to do), answers 403 FEATURE_NOT_IN_PLAN. A saved report that already uses them keeps running; a report saved with its definition unchanged (a rename, say) and a dashboard saved with its filter unchanged (after arranging its tiles, say) save as usual; and the firewall templates are listed with available: false.

Permissions. Any member can run reports and create saved reports and dashboards. A private report or dashboard is visible to its owner only (404 for anyone else). The owner can change or delete it; owners and admins can also change or delete any account-visible one. Each record carries canEdit for the caller.

Secret and personal tags. A report that groups by, counts or filters on a tag key classified secret is refused with 400 INVALID_REPORT. Values of keys classified personal are masked for callers who may not view personal tags, and the key is listed in meta.masked.

IP addresses. A client’s IP address is personal data. Grouped by ip_address, a caller who may not view personal tags gets each row’s value as an opaque id (ip_ and 16 hex digits, stable for the account) and its label as the network part only (203.0.113.x; an IPv6 address keeps its first three groups, the /48), and meta.masked lists ip_address. Reports cannot filter on an IP address; they can filter on the country (country == "DE"). An embed never shows IP addresses.

Filters hidden from the caller. A saved filter that names a personal tag key would show its values, so for a caller who may not view personal tags a saved report, each of its revisions and a dashboard come back with "filterHidden": true (at the top level of the record or revision) and an empty filter (definition.filter or filter is ""). Such a report can’t run for that caller, and a dashboard’s tiles can’t run for them either (see POST /savedReports/{reportSid}/run). Writing an empty filter keeps a hidden one as it is; removeHiddenFilter: true on PATCH clears it.

All examples use Authorization: Bearer .

The report definition

A report definition (ReportRequest) is the body of POST /reports/query and the definition of a saved report.

  • source (body, string): What the report counts: requests (the default, also when left out) or spans, the APM operations inside requests (database queries, Redis and cache calls, outbound HTTP, queue, RPC and GraphQL work). A span report takes the span dimensions, metrics and filter fields instead of the request ones

  • timeRange (body, object, required): { "relative": "last_7d" } (one of last_1h, last_24h, last_7d, last_30d, last_90d, today, yesterday, this_month, previous_month; UTC days), or { "start": "…", "end": "…" } in ISO 8601. At most 400 days; an end in the future is cut to now

  • groupBy (body, string[]): Up to 2 dimensions: a request field (method, host, endpoint, endpoint_kind, path, status_code, status_class (1xx, 2xx, 3xx, 4xx, 5xx, or unknown for a missing status or one below 100), direction, bucket_sid, environment_sid, context_sid, ai_kind, ai_provider, ai_model, rpc_method, mcp_tool), tag:, attr:: (a catalogue attribute of a tag’s values), or a firewall dimension: waf_action (block, would-block or tag), waf_policy (the deciding policy’s sid; labelled with its description), waf_rule (the deciding rule’s id; labelled with its message) or waf_matched (yes or no). Requests the firewall did not match have "" for waf_action, waf_policy and waf_rule. Or a client dimension, both recorded on each request when it is received: ip_address (the client’s IP address, labelled with it; "" without one) or country (the ISO 3166-1 alpha-2 code of the country the address was located in, labelled with its name; "", labelled (none), without one). Requests received before they were recorded are "", and meta.approximations says from when they are recorded. Omit for totals

  • metrics (body, string[]): 1 to 10 of count, error_count, server_error_count, error_rate, avg, p50, p90, p95, p99, max (latency in ms), request_bytes, response_bytes, distinct: (not an attribute; distinct:ip_address counts client IP addresses, distinct:country countries), sum: and avg: for a numeric field (duration_ms, request_size, response_size, db_query_count, db_time_ms, redis_op_count, redis_time_ms, cache_op_count, cache_time_ms, http_out_count, http_out_time_ms, queue_op_count, queue_time_ms, rpc_op_count, rpc_time_ms, span_count, spans_dropped, ai_message_count, ai_tool_count, ai_input_tokens, ai_output_tokens, ai_cached_input_tokens), and the firewall metrics waf_matched_count, waf_blocked_count, waf_would_block_count (request counts), avg:waf_score and waf_score_max (the anomaly score of matched requests). Default ["count"]. distinct: metrics can’t be combined with an attribute dimension. Firewall dimensions and metrics, ip_address and country are read from stored requests (at most 31 days at a time)

  • filter (body, string): Criteria in the request search language, up to 4,000 characters, such as status_code >= 500 AND tag:plan == "enterprise" or waf_action == "block". Conditions on bodies are refused (400 INVALID_FILTER_CRITERIA)

  • scope (body, object): bucketSid, environmentSid and direction (inbound or outbound); each optional

  • interval (body, string): auto (about 200 points), 1m, 5m, 15m, 1h, 6h or 1d. Adds a series to each row. At most 500 points. Omit for totals only

  • compareTo (body, string): previous_period, same_period_last_week or same_period_last_month

  • orderBy (body, object): { "metric": "", "dir": "desc" | "asc" }. Default: count if asked for, else the first metric, descending

  • limit (body, number): Top rows of the first dimension, 1 to 100. Default 25

  • secondaryLimit (body, number): Top values of the second dimension within each row, 1 to 50. Default 10

  • includeOther (body, boolean): Add an Other row for everything outside the top rows, so rows add up to the totals. Default true

  • resolveLabels (body, boolean): Add catalogue labels for tag values in labels. Default true

  • precision (body, string): fast (default) prefers aggregates; exact reads stored requests

A report may hold at most 20,000 values (rows × second-dimension values × points); a larger one is refused with a 400 that says what to lower.

Span reports

With "source": "spans" a report counts spans instead of requests (Span reports). Everything else in the definition (time range, interval, comparison, limits, Other, ordering) works the same, with these differences:

  • Plan. Span reports need APM as well as Custom reports: running or saving one without it answers 403 FEATURE_NOT_IN_PLAN with data.feature: "apmEnabled". Accounts without a span source (no spans sent yet) get an empty result whose meta.reasons says so.
  • Window. Spans are kept for a shorter time than requests: the window is first cut to the account’s span retention (meta.clamped, meta.clampReason: "retention"), and a span report reads at most 7 days of stored spans; a longer window answers 400 INVALID_REPORT, except on a dashboard tile run (dashboardSid), where it is cut to its last 7 days instead (meta.clamped: true, meta.clampReason: "span_window", meta.clampDays: 7, and a sentence in meta.reasons), on every plan. A comparison window then follows the shortened window, and an auto interval is picked for it. scope.direction: "outbound" is refused (spans belong to the inbound requests that ran them; outbound calls are http.out spans).
  • Dimensions (groupBy, also filter fields): type (span type), fingerprint (operation; rows carry the operation’s name in labels.fingerprint, never statement text), name, op (verb), target, system, db, host (peer host), out_target (outbound method, host and path), out_status_class (2xx to 5xx, none for no response), error_class, error_type, endpoint (the endpoint that ran the span), call_site, service_version, instance_sid, bucket_sid, environment_sid. tag: and attr: dimensions are refused: tags are not recorded on spans yet.
  • Metrics: count (calls), error_count (failed calls), error_rate, total_time (ms), avg, p50, p90, p95, p99, max (duration in ms), requests (requests with the operation, approximate; estimated under sampling), calls_per_request (N+1: stored spans per stored request, which sampling does not bias), out_client_error_count, out_server_error_count, out_no_response_count (http.out spans only), sum:rows, avg:rows, distinct:. server_error_count, request_bytes, response_bytes and the request sum:/avg: fields are refused. Rows can be picked in SQL by count, error_count and total_time.
  • Sampling. Counts and times are weighted by each span’s sample weight, so they estimate every call when spans are sampled; meta.sampled is true (and approximations says so) only when sampling was in effect for the window. Percentiles come from fixed latency buckets (approximate) unless precision is exact.
  • Filter: the same grammar as the request search language, over the dimensions above plus duration_ms and rows (numbers), out_status_code (number, http.out only) and ok (true or false), for example type == "db.query" AND duration_ms > 100. tag: and request fields are refused with 400 INVALID_FILTER_CRITERIA.
  • Embeds. Span reports, and dashboards with span tiles, can’t be embedded yet: turning embedding on or minting a token for one answers 400.
bash
curl -X POST https://api.apitraffic.io/v1/accounts/{accountSid}/reports/query \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{
    "source": "spans",
    "timeRange": { "relative": "last_7d" },
    "filter": "type == \"db.query\"",
    "groupBy": ["fingerprint"],
    "metrics": ["total_time", "p95", "count"],
    "orderBy": { "metric": "total_time" },
    "limit": 1
  }'
json
{
  "rows": [
    { "dims": { "fingerprint": "fpr_3c1e9a0b5d7f2468" }, "labels": { "fingerprint": "SELECT users" },
      "metrics": { "total_time": 184220.5, "p95": 41.2, "count": 12840 } },
    { "dims": { "fingerprint": "__other__" }, "labels": { "fingerprint": "Other" }, "other": true,
      "metrics": { "total_time": 22104, "p95": 12.8, "count": 9311 } }
  ],
  "totals": { "total_time": 206324.5, "p95": 38.9, "count": 22151 },
  "meta": {
    "source": "raw", "dataset": "spans", "sampled": false,
    "segments": [ { "source": "raw", "from": "2026-10-13T09:00:00.000Z", "to": "2026-10-20T09:00:00.000Z" } ],
    "exact": false, "approximations": [ "Percentiles are estimated from fixed latency buckets." ],
    "otherShare": 0.420342, "missingValueShare": null, "masked": [], "clamped": false,
    "window": { "from": "2026-10-13T09:00:00.000Z", "to": "2026-10-20T09:00:00.000Z" },
    "intervalSeconds": null, "reasons": [ "Stored spans are read." ], "queries": 2
  }
}

The report result

  • rows (array): One per group, ordered by orderBy with Other last. Each has dims (dimension → value; __other__ for the Other row, "" for requests without the tag, firewall decision or client IP address), labels (dimension → catalogue label, a firewall policy’s or rule’s name, an IP address (masked: its network part), or a country’s name), other: true on the Other row, metrics (metric → number or null), with compareTo a compare (metric → value (the earlier period’s), delta, delta_pct), and with interval a series (one entry per point: t and each metric)

  • totals (object): Each metric over every request in scope

  • compareTotals (object): With compareTo: the totals’ comparison, shaped like a row’s compare

  • points (string[]): With interval: the start of each point, ISO 8601

  • meta (object): How the numbers were made: dataset (requests or spans: what was counted), source (aggregates, raw or mixed: where it was read from), aggregateFamily (with aggregates: requests or endpoint, the pre-aggregated rows read), sampled (span reports: whether sampling was in effect, so counts are estimates), segments (source, from, to for each range read), exact, approximations (sentences), otherShare (0 to 1 of requests folded into Other), missingValueShare (0 to 1 of requests without the grouped tag), masked (tag keys shown masked, and ip_address when IP addresses are), attribution (text and url: the credit for the geo data, present when the report shows countries; show it with them), clamped (true when the window was cut to the account’s retention or, for a span tile on a dashboard, to the last days span reports read), clampReason and clampDays (span reports only, when clamped: retention with the days spans are kept, or span_window with the days a span report reads), window and compareWindow (from, to), intervalSeconds, reasons (why this source), clientIpMode (truncated or off, when the report reads IP addresses and the account keeps networks only or none), promotable (the tag key whose promotion alone would let pre-aggregated data answer this report: a registered key, not promoted or classified secret, that the report groups by, with nothing else in the report that pre-aggregated data does not hold; otherwise empty, and always empty for span reports and for reports with a firewall or client dimension or a firewall metric, which only stored requests answer), queries

Run a report

POST https://api.apitraffic.io/v1/accounts/{accountSid}/reports/query

Run a report definition once, without saving it. The body is a report definition, plus explain: true to add plan to the response (the path, the pre-aggregated rows read, segments and reasons the planner chose).

bash
curl -X POST https://api.apitraffic.io/v1/accounts/{accountSid}/reports/query \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{
    "timeRange": { "relative": "last_7d" },
    "groupBy": ["tag:customerId"],
    "metrics": ["count", "error_rate", "p95"],
    "compareTo": "previous_period",
    "limit": 2
  }'
json
{
  "rows": [
    { "dims": { "tag:customerId": "org_8f2a" }, "labels": { "tag:customerId": "Acme Corp" },
      "metrics": { "count": 182341, "error_rate": 0.0042, "p95": 412 },
      "compare": { "count": { "value": 150002, "delta": 32339, "delta_pct": 21.56 },
                   "error_rate": { "value": 0.0051, "delta": -0.0009, "delta_pct": -17.65 },
                   "p95": { "value": 398, "delta": 14, "delta_pct": 3.52 } } },
    { "dims": { "tag:customerId": "org_19c0" }, "labels": {}, "metrics": { "count": 90211, "error_rate": 0.0011, "p95": 230 },
      "compare": { "count": { "value": 88000, "delta": 2211, "delta_pct": 2.51 },
                   "error_rate": { "value": 0.001, "delta": 0.0001, "delta_pct": 10 },
                   "p95": { "value": 240, "delta": -10, "delta_pct": -4.17 } } },
    { "dims": { "tag:customerId": "__other__" }, "labels": { "tag:customerId": "Other" }, "other": true,
      "metrics": { "count": 4120, "error_rate": 0.002, "p95": 300 },
      "compare": { "count": { "value": 3900, "delta": 220, "delta_pct": 5.64 },
                   "error_rate": { "value": 0.002, "delta": 0, "delta_pct": 0 },
                   "p95": { "value": 310, "delta": -10, "delta_pct": -3.23 } } }
  ],
  "totals": { "count": 276672, "error_rate": 0.0032, "p95": 380 },
  "compareTotals": { "count": { "value": 241902, "delta": 34770, "delta_pct": 14.37 },
                     "error_rate": { "value": 0.0038, "delta": -0.0006, "delta_pct": -15.79 },
                     "p95": { "value": 377, "delta": 3, "delta_pct": 0.8 } },
  "meta": {
    "source": "aggregates",
    "segments": [ { "source": "aggregates", "from": "2026-10-13T09:00:00.000Z", "to": "2026-10-20T09:00:00.000Z" } ],
    "compareSource": "aggregates",
    "exact": false,
    "approximations": [ "Percentiles are estimated from fixed latency buckets.", "Aggregates keep each minute's busiest values; the rest are counted as Other." ],
    "otherShare": 0.014891,
    "missingValueShare": 0.031,
    "masked": [],
    "window": { "from": "2026-10-13T09:00:00.000Z", "to": "2026-10-20T09:00:00.000Z" },
    "compareWindow": { "from": "2026-10-06T09:00:00.000Z", "to": "2026-10-13T09:00:00.000Z" },
    "intervalSeconds": null,
    "reasons": [ "customerId's aggregates cover the whole window." ],
    "promotable": [],
    "queries": 4
  }
}

A definition the engine refuses answers 400 INVALID_REPORT with the reason in message. When the only problem is that it reads more than 31 days of stored requests and promoting the tag key it groups by would let pre-aggregated data answer it, data.promotable names that key ({ "promotable": ["customerId"] }); promote it with the Dimensions API. Failed reads answer like other analytics reads: 504 TELEMETRY_TIMEOUT (shorten the window, add a filter or promote the key), 503 TELEMETRY_OVERLOADED with Retry-After.

Templates

GET https://api.apitraffic.io/v1/accounts/{accountSid}/reportTemplates

The template gallery. {customerKey} in a template’s definition is replaced with the account’s customer key. Without one, the templates that need it are unavailable.

  • records (array): Each: key, name, description, visualization, definition (a report definition), requires (for example ["customerKey"]), available, and unavailableReason when not available
json
{
  "records": [
    { "key": "endpoint_overview", "name": "Endpoint overview", "description": "Requests, error rate and p95 per endpoint.",
      "visualization": "table", "requires": [], "available": true,
      "definition": { "timeRange": { "relative": "last_24h" }, "groupBy": ["endpoint"], "metrics": ["count", "error_rate", "p95"] } },
    { "key": "top_customers", "name": "Top customers by volume", "description": "Your busiest customers.",
      "visualization": "bar", "requires": ["customerKey"], "available": false,
      "unavailableReason": "Pick a customer key in Settings → Dimensions first.",
      "definition": { "timeRange": { "relative": "last_30d" }, "groupBy": ["tag:{customerKey}"], "metrics": ["count", "error_rate", "p95"] } }
  ]
}

Keys: endpoint_overview, top_customers, customer_errors, customer_latency_trend, endpoint_usage_by_customer, active_customers, usage_by_plan, outbound_vendor_usage, llm_tokens_by_customer, and the span report templates slowest_queries, outbound_by_peer, errors_by_span_type and n_plus_one, listed only when the plan includes APM.

Saved reports

A saved report: sid (rpt_…), name, description, definition, visualization (table, line, stacked_area, bar, stacked_bar or number), display (how it is drawn beyond its visualization, with the defaults filled in: { "table": "collapsed" } unless set), visibility (private or account), ownerUserSid, folder, pinned, embeddable, definitionVersion, templateKey, createdAt, updatedAt and canEdit.

GET https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports

Your private reports and every account-visible one.

  • folder (query, string): Only reports in this folder

  • pinned (query, boolean): Only pinned (true) or unpinned (false) reports

Returns { "records": [ …saved reports ] }.

POST https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports

Save a report. Returns 201 with the report.

  • name (body, string, required): 1 to 120 characters

  • description (body, string): Up to 1,000 characters

  • definition (body, object, required): A report definition, checked as POST /reports/query checks it. Use a relative timeRange so the report always covers recent traffic

  • visualization (body, string, required): table, line, stacked_area, bar, stacked_bar or number

  • display (body, object): How the report is drawn beyond its visualization. Its only field, table, sets the data table under a chart or a single number: hidden (only the chart; screen readers still get the table), collapsed (behind Show the numbers, the default) or shown. A table report ignores it. Any other field or value is 400. null (or {}) means the defaults. The builder, dashboard tiles and embeds all follow it

  • visibility (body, string): private (default) or account

  • folder (body, string): Up to 80 characters; null for none

  • pinned (body, boolean): List it first in Reports

  • templateKey (body, string): The template it was made from

bash
curl -X POST https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{
    "name": "Errors by customer",
    "definition": { "timeRange": { "relative": "last_7d" }, "groupBy": ["tag:customerId"], "metrics": ["error_count", "error_rate"],
                    "orderBy": { "metric": "error_count", "dir": "desc" } },
    "visualization": "table",
    "visibility": "account",
    "folder": "Support"
  }'
json
{
  "sid": "rpt_6Hq2…", "name": "Errors by customer", "description": null,
  "definition": { "timeRange": { "relative": "last_7d" }, "groupBy": ["tag:customerId"], "metrics": ["error_count", "error_rate"],
                  "orderBy": { "metric": "error_count", "dir": "desc" } },
  "visualization": "table", "display": { "table": "collapsed" }, "visibility": "account", "ownerUserSid": "usr_…", "folder": "Support", "pinned": false,
  "definitionVersion": 1, "templateKey": null, "createdAt": "2026-10-20T09:00:00.000Z", "updatedAt": "2026-10-20T09:00:00.000Z", "canEdit": true
}

GET https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports/{reportSid}

One saved report. 404 for a report that is private to someone else. It also carries onEmbeddedDashboard: true when an embeddable dashboard shows it, so only owners and admins may change its definition.

PATCH https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports/{reportSid}

Change any of the fields POST takes. A change to definition, visualization or name keeps the previous version as a revision and adds 1 to definitionVersion. A change to display does not (it replaces the whole object; null resets it), and while the report is embedded anyone who can edit it may still change it. Returns the report.

  • removeHiddenFilter (body, boolean): true clears the report’s stored filter. Meant for a filter hidden from the caller (filterHidden): it is deleted and lost. A non-empty filter in the same request’s definition wins and is stored instead. A definition whose filter is empty, without this flag, keeps a hidden filter. While a hidden filter stays, changing a shared report’s definition is refused with 400 INVALID_REPORT (“This shared report filters on a personal tag you can’t view, so its settings can’t change while that filter stays. Remove the filter (it will be lost), or ask someone who can view personal tags.”)

  • embeddable (body, boolean): true lets embed tokens show this report. Only an account owner or admin (or an administrator API token) can turn it on, and only on an account-visible report: otherwise 403 NOT_AUTHORIZED or 400 INVALID_PARAMETERS. Turning it on needs the embedsEnabled plan feature (403 FEATURE_NOT_IN_PLAN) and a free embeddedViews slot (409 ENTITLEMENT_LIMIT_REACHED). false stops every embed of it; anyone who can edit it may turn it off. Making it private turns it off too.

bash
curl -X PATCH https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports/rpt_6Hq2… \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{ "visualization": "bar", "pinned": true }'

DELETE https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports/{reportSid}

Delete a report and its revisions. Dashboard tiles that show it are removed. Returns 204. This works after a downgrade too, so you can clean up what you saved.

GET https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports/{reportSid}/revisions

Every version, newest first, at most 50. The first is the current version (its version equals the report’s definitionVersion). A version whose filter is hidden from the caller has "filterHidden": true and an empty definition.filter.

json
{
  "records": [
    { "version": 1, "name": "Errors by customer", "visualization": "table",
      "definition": { "timeRange": { "relative": "last_7d" }, "groupBy": ["tag:customerId"], "metrics": ["error_count", "error_rate"] },
      "changedBy": "usr_…", "createdAt": "2026-10-20T09:00:00.000Z" }
  ]
}

POST https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports/{reportSid}/run

Run a saved report, optionally with overrides. This is how a dashboard runs its tiles. Every field is optional.

A report whose own filter names a personal tag the caller may not view is refused with 400 INVALID_REPORT (“Values of the tag key ‘…’ are personal and masked for your role: a report cannot filter on them.”).

  • dashboardSid (body, string): The dashboard this run is a tile of. Send it with every tile run: when that dashboard’s filter is hidden from the caller (filterHidden), the run is refused with 400 INVALID_REPORT (“This dashboard filters on a personal tag you can’t view, so its tiles can’t run for you.”) rather than run without the filter. A dash_… sid; a dashboard that was deleted or is no longer visible to the caller answers 400 INVALID_REPORT (“This dashboard was deleted or is no longer shared with you.”)

  • timeRange (body, object): Replaces the report’s time range

  • filter (body, string): Added to the report’s own filter: the report counts requests that match both ((report filter) AND (this filter)). For a span report it is in the span filter language, except on a tile run (dashboardSid): a dashboard’s filter is request language, and on a span tile only endpoint and instance conditions apply. Any other field answers 400 INVALID_REPORT (“This dashboard’s filter uses status_code, which spans don’t have.”), as does a dashboard scoped to outbound traffic. A filter on the firewall’s decisions (waf_action == "block", tag:waf.matched == "1") needs the firewall in the plan (403 FEATURE_NOT_IN_PLAN), except on a tile run of the dashboard it is saved on (dashboardSid, the report one of its tiles, and the dashboard’s own filter): that keeps running after a downgrade, as the saved report does

  • scope (body, object): bucketSid, environmentSid, direction: each one set replaces the report’s

bash
curl -X POST https://api.apitraffic.io/v1/accounts/{accountSid}/savedReports/rpt_6Hq2…/run \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{ "dashboardSid": "dash_3Nf8…", "timeRange": { "relative": "last_24h" }, "filter": "tag:customerId == \"org_8f2a\"", "scope": { "environmentSid": "env_…" } }'

Returns a report result whose meta also has report: { "sid", "name", "visualization", "display" }.

Dashboards

A dashboard: sid (dash_…), name, description, visibility (private or account), ownerUserSid, filter (criteria applied to every tile), scope (bucketSid, environmentSid, direction), timeRange (default { "relative": "last_24h" }), layout, embeddable, createdAt, updatedAt and canEdit, plus "filterHidden": true (with filter as "") when the filter names a personal tag the caller may not view.

layout is { "tiles": [ … ] }, at most 24 tiles on a grid 12 columns wide. Each tile: id (unique in the dashboard), reportSid, x (0 to 11), y (0 or more), w (1 to 12, with x + w at most 12), h (1 to 8) and an optional title.

GET https://api.apitraffic.io/v1/accounts/{accountSid}/dashboards

Your private dashboards and every account-visible one. Returns { "records": [ …dashboards ] }.

POST https://api.apitraffic.io/v1/accounts/{accountSid}/dashboards

Create a dashboard. Returns 201 with the dashboard.

  • name (body, string, required): 1 to 120 characters

  • description (body, string): Up to 1,000 characters

  • visibility (body, string): private (default) or account

  • filter (body, string): Criteria applied to every tile, up to 4,000 characters; "" for none. Checked on write (400 INVALID_FILTER_CRITERIA)

  • scope (body, object): bucketSid, environmentSid, direction

  • timeRange (body, object): As in a report definition

  • layout (body, object): { "tiles": [ … ] }. Every reportSid must be a report the caller can see; an account-visible dashboard may only hold account-visible reports (400 otherwise)

bash
curl -X POST https://api.apitraffic.io/v1/accounts/{accountSid}/dashboards \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{
    "name": "Customer health",
    "visibility": "account",
    "filter": "tag:customerId == \"org_8f2a\"",
    "timeRange": { "relative": "last_7d" },
    "layout": { "tiles": [
      { "id": "t1", "reportSid": "rpt_6Hq2…", "x": 0, "y": 0, "w": 6, "h": 3 },
      { "id": "t2", "reportSid": "rpt_9Kd1…", "x": 6, "y": 0, "w": 6, "h": 3 }
    ] }
  }'
json
{
  "sid": "dash_3Nf8…", "name": "Customer health", "description": null, "visibility": "account", "ownerUserSid": "usr_…",
  "filter": "tag:customerId == \"org_8f2a\"", "scope": {}, "timeRange": { "relative": "last_7d" },
  "layout": { "tiles": [
    { "id": "t1", "reportSid": "rpt_6Hq2…", "x": 0, "y": 0, "w": 6, "h": 3 },
    { "id": "t2", "reportSid": "rpt_9Kd1…", "x": 6, "y": 0, "w": 6, "h": 3 }
  ] },
  "createdAt": "2026-10-20T09:00:00.000Z", "updatedAt": "2026-10-20T09:00:00.000Z", "canEdit": true
}

GET https://api.apitraffic.io/v1/accounts/{accountSid}/dashboards/{dashboardSid}

One dashboard. 404 for a dashboard that is private to someone else. To show it, run each tile with POST /savedReports/{reportSid}/run, passing the dashboard’s sid as dashboardSid, and its timeRange, filter and scope. When the dashboard has filterHidden, its tiles can’t run for the caller.

PATCH https://api.apitraffic.io/v1/accounts/{accountSid}/dashboards/{dashboardSid}

Change any of the fields POST takes; the same checks apply. Returns the dashboard. An empty filter keeps a filter hidden from the caller as it is.

  • removeHiddenFilter (body, boolean): true clears the dashboard’s stored filter, so its tiles can run for the caller. Meant for a filter hidden from them (filterHidden): it is deleted and lost. A non-empty filter in the same request wins and is stored instead

  • embeddable (body, boolean): true lets embed tokens show this dashboard and its tiles. Only an account owner or admin (or an administrator API token) can turn it on, and only on an account-visible dashboard: otherwise 403 NOT_AUTHORIZED or 400 INVALID_PARAMETERS. Turning it on needs the embedsEnabled plan feature (403 FEATURE_NOT_IN_PLAN) and a free embeddedViews slot (409 ENTITLEMENT_LIMIT_REACHED). false stops every embed of it; anyone who can edit it may turn it off. Making it private turns it off too.

bash
curl -X PATCH https://api.apitraffic.io/v1/accounts/{accountSid}/dashboards/dash_3Nf8… \
  -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
  -d '{ "filter": "tag:customerId == \"org_19c0\"", "timeRange": { "relative": "last_30d" } }'

DELETE https://api.apitraffic.io/v1/accounts/{accountSid}/dashboards/{dashboardSid}

Delete a dashboard. Its reports are kept. Returns 204. This works after a downgrade too.