Skip to content

Report builder ​

Español: Constructor de reportes.

The report builder answers questions about the calls a tenant placed, over a closed catalog of dimensions and metrics. Three routes carry it: GET /v1/report-catalog (the vocabulary), POST /v1/reports/run (one aggregate, or one page of attempts) and /v1/report-definitions (saved queries). Field names, enums, limits and error codes below are copied from the vendored OpenAPI document (core v0.105.0, openapi/dialer-v1.yaml); the API Reference generated from it is the authority on shapes.

Availability ​

CapabilitySpec operation
Read the cataloggetReportCatalog
Run an aggregate or a detail page, JSON or CSVrunReport
Saved definitions: list, create, read, replace, deletelistReportDefinitions, createReportDefinition, getReportDefinition, replaceReportDefinition, deleteReportDefinition

Running is gated by the tenant flag report_builder_enabled: with the flag off, POST /v1/reports/run answers 503 report_builder_disabled and nothing is read (runReport). The catalog keeps answering with the flag off (getReportCatalog), and the saved definitions are not gated by it (listReportDefinitions). Do not assume the flag is on for your tenant: call the run route and handle the 503.

Credentials: the routes are tenant-scoped, so a dd_ key or a dashboard login works (runReport). A dds_ support session is GET-only and cannot run reports.

The catalog ​

bash
curl -s "$API/v1/report-catalog" -H "Authorization: Bearer $TOKEN"

The 200 body is the ReportCatalog schema: fact (always call_attempts), version (it bumps when a published definition changes), dimensions, metrics, detail_columns and limits.

  • The unit is the attempt: the original dial the platform placed plus the corrections appended to it. disposition is that of the last settling correction, and in_flight while there is none (getReportCatalog).
  • Dimensions published in core v0.105.0: campaign_id, agent_id, disposition, source, trunk_kind, amd_verdict, ai_used, hangup_cause, day, week, month.
  • Metrics: attempts, contacts, contact_rate_bp (basis points), accounts_attempted, talk_seconds, ai_calls. Each metric has one definition, shared with the fixed counters of GET /v1/stats/summary.
  • No dimension carries personal data: pii is null on every one.
  • Limits (the example in getReportCatalog): max_window_days: 31, max_groups: 1000, max_dimensions: 4, max_filter_values: 100, max_detail_rows: 1000. Read the live values from the catalog rather than hard-coding them.

The key lists above come from lib/dialer/reports/catalog.ex at the v0.105.0 tag of core; treat GET /v1/report-catalog as the source of truth, because a later version can add keys.

Run an aggregate ​

bash
curl -s -X POST "$API/v1/reports/run" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "from": "2026-10-01T00:00:00Z",
    "to":   "2026-10-08T00:00:00Z",
    "dimensions": ["campaign_id", "disposition"],
    "metrics": ["attempts", "contacts", "contact_rate_bp"],
    "filters": { "source": ["platform"] },
    "zone": "UTC"
  }'

Rules from runReport and the ReportRunRequest schema:

  • from and to are mandatory RFC 3339 timestamps. The window is [from, to) on started_at and at most 31 days.
  • Up to 4 dimensions (none gives a single totals row) and at least one metric.
  • filters maps a filterable dimension to a non-empty list of at most 100 values.
  • zone defaults to UTC; it is an IANA Area/Location name and sets the calendar of day, week and month.
  • Any field outside from, to, dimensions, metrics, filters and zone (for example tenant_id) is 422 unknown_field: the tenant is the caller's.
  • contact_rate_bp is contacts over attempts in basis points, rounded half up, and null (never 0) when there are no attempts.

The 200 body is the ReportRunResult schema: fact, catalog_version, from, to, zone, columns (each with kind dimension or metric), row_count and rows.

CSV ​

Add ?format=csv. An Accept header alone is not enough (runReport). The CSV has one header row of column keys, neutralises spreadsheet formulas and carries the header X-Artifact-SHA256, the SHA-256 of the CSV body. Compare it with your own hash of the body to detect a truncated or altered download:

bash
curl -s -D headers.txt -o report.csv -X POST "$API/v1/reports/run?format=csv" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"from":"2026-10-01T00:00:00Z","to":"2026-10-08T00:00:00Z","metrics":["attempts"]}'
grep -i '^x-artifact-sha256' headers.txt
sha256sum report.csv

Detail mode: one row per attempt ​

"mode": "detail" answers one row per placed attempt instead of a group (runReport). It takes columns (from detail_columns of the catalog), filters, unmask, limit (default and maximum 1000) and after.

json
{ "mode": "detail", "from": "2026-10-01T00:00:00Z", "to": "2026-10-02T00:00:00Z",
  "columns": ["attempt_id", "to_number"], "limit": 500 }
  • Pagination is a keyset over (started_at, id), oldest first. The page carries next_cursor; send it as after for the next page. next_cursor is null on the last page. A malformed after is 400 invalid_cursor.
  • dimensions, metrics and zone are aggregate-only; columns, unmask, limit and after are detail-only. The wrong mode's key is 422 field_not_for_mode.
  • The free-text note and compliance_snapshot are not columns.
  • With ?format=csv, every row carries an export_cursor column (the last one is the after of the next page) and the response carries X-Artifact-SHA256.

Transfer columns ​

Two detail columns describe a transferred call. They are on core main (core#2403, core ADR #297), after the v0.129.0 tag, and the catalog version is 3 there (@version 3 in lib/dialer/reports/catalog.ex); the definitions are in the same file, key transfer_count and key transfer_chain. Read detail_columns of the live catalog to see whether your deployment has them.

  • transfer_count (integer): the control hand-offs of the call, the applied control_handoff rows of call_control_actions. 0 for a call nobody transferred.
  • transfer_chain (string): who held the call, in order: the agent it started with (unassigned when it started with none), then the agent of each hand-off, joined by >; external where control left the agent population. null for a call nobody transferred. The legs with role and duration are transfer_chain of GET /v1/calls/{call_id} (see the REST API guide).

Neither column carries personal data (pii is null). In CSV they are plain columns like the rest.

Phone masking and unmask ​

A column whose pii is phone or identifier is masked by default: *** plus the last four characters, read from the stored mask without decrypting anything (getReportCatalog).

"unmask": true returns those columns whole. It is allowed only for a dd_ key or a ddu_ login with the account_admin role. Otherwise (runReport):

CallerAnswer
member role403 role_forbidden (never a silently masked answer)
dds_ support session403 forbidden
unmask with no PII column in columns422 unmask_without_pii_column

Every unmasked page writes one row to an append-only evidence table in the same transaction as the read: actor, the definition, the columns, the unmasked columns and the row count, never the values. If that row cannot be written, nothing is returned. In CSV an unmasked number comes out behind the formula guard (prefixed with '). The unmasked field of the response lists the PII columns returned whole, and is empty when they were masked (ReportRunResult).

Saved definitions ​

A definition is a query over the catalog (dimensions, metrics, filters), never its result: the window and the zone are parameters of each run (listReportDefinitions).

RoutePurpose
GET /v1/report-definitionsList the tenant's definitions, by name
POST /v1/report-definitionsSave one; 201 with { "report_definition": {...} }
GET /v1/report-definitions/{id}Read one
PUT /v1/report-definitions/{id}Full replacement of name, dimensions, metrics, filters
DELETE /v1/report-definitions/{id}Delete; 204
  • The body is validated against the catalog exactly like an inline run. A key the catalog does not publish is 422 and the message names it.
  • name is 1 to 120 characters and unique per tenant: a duplicate is 409 name_taken.
  • The tenant and author are the caller's. tenant_id or created_by in the body is 422 unknown_field. created_by is null when a dd_ key saved it.
  • fact and catalog_version are stamped by the server; a PUT refreshes catalog_version.
  • Tenant isolation. The definitions table has row-level security with a tenant_isolation policy on the tenant id, so another tenant's definition is a 404, not a 403 (runReport; migration create_report_definitions in core v0.105.0).

Run a saved definition with definition_id in place of dimensions, metrics and filters:

json
{ "definition_id": "<uuid>", "from": "2026-10-01T00:00:00Z", "to": "2026-10-08T00:00:00Z" }

from, to and zone are always the run's own. A run by definition_id is always an aggregate.

Errors ​

Errors use the standard envelope { "error": { "code", "message" } }. As the contract returns them (runReport):

StatuscodeWhen
400window_too_largeThe window is wider than 31 days. A to not after from, or a missing or non-RFC 3339 bound, is also 400
400invalid_cursorafter is not a valid cursor
422too_many_groupsThe answer would exceed 1000 groups. The request is refused whole, never cut silently
422catalog_changedA saved definition names a key the catalog no longer publishes. The message names it and nothing is answered, not even a partial result
422definition_conflictdefinition_id together with dimensions, metrics or filters
422invalid_definition_idMalformed definition_id
422unknown_field, unknown_dimension, unknown_metric, unknown_column, unknown_filterA key outside the closed vocabulary
422invalid_dimensions, invalid_metrics, invalid_columns, invalid_filters, invalid_zone, invalid_mode, invalid_limit, invalid_unmaskA list bound or a value out of range
422field_not_for_modeA detail-only key in an aggregate or the reverse
403role_forbiddenunmask: true from a member
404(not found)Unknown definition_id, or another tenant's
429rate_limitedOver the per-principal brake; honour Retry-After
503report_builder_disabledThe tenant flag is off
503report_timeoutThe read exceeded its statement timeout

On catalog_changed, open the catalog, update the saved definition with a PUT that uses current keys, and run again.

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.