Skip to content

Roadmap ​

Español: Hoja de ruta.

Assume nothing here is served unless the entry says shipped or live

This page is the register of designed-but-unbuilt surfaces, kept so a design is never confused with the live API. If an entry is not explicitly marked shipped or live, the current core does not serve it — do not build against it yet. Entries that were designed here and delivered afterwards stay on the page, marked and linked to their reference, so the record of what was promised and what landed is one page. The implemented surface is exactly what REST API and WebSocket events describe.

Outbound webhooks ​

Server-to-server push of platform facts to your endpoints — shipped in core (MT-CTI-04, phases 1-4): the endpoint registry /v1/webhooks, an outbox written in the same transaction as the fact, a dispatcher with HMAC signatures, a capped retry ladder and an append-only delivery log. The integration guide is Outbound webhooks (CRM integration).

It has been in a published core release since v0.36.0 and is in the vendored spec this site generates its API Reference from, which is the authority on request and response shapes; the guide stays the integration walkthrough.

Not to be confused with the inbound SMS webhooks, which are live: the platform hosts signed endpoints that SMS providers call with delivery receipts and consumer replies — see Webhooks in the API Reference.

What the design preview on this page promised, and what was actually built — the record of both, since that is what this page is for:

Promised hereBuilt
Payloads: dialer.v1 messages as protojson (CallEvent, CallDetailRecord, ComplianceDecision)A v1 envelope {event, event_id, occurred_at, tenant_id, data} whose data is the public /v1 projection of the resource — the same shape its GET answers, not protojson. Compliance decisions are not an event; the six-name catalog is call.ended, call.answered, promise.recorded, sms.received, sms.optout, recording.available.
Routing header X-DD-Event-TypeX-DD-Event
Unique X-DD-Delivery-IDX-DD-Delivery-Id — the receiver's idempotency key, as promised
X-DD-Signature: t=<unix>,v1=<hex> over "<t>.<raw body>", constant-time, ~5-minute toleranceAs promised, plus X-DD-Signature-Previous during a 24 h rotation window
Respond 2xx within 10 s15 s for the whole request, 5 s of it for the connect
Retries with exponential backoff; at-least-once; no cross-delivery orderingAs promised. The ladder is capped and bounded by a maximum attempt count, then dead-letters; dedupe is on X-DD-Delivery-Id (delivery) and event_id (fact), not on callId/decisionId.
Self-service endpoint CRUD + delivery log per tenantAs promised: CRUD, secret rotation, GET /v1/webhooks/{id}/deliveries with every attempt, and a manual redelivery that appends.

Protojson alignment ​

The contracts repo defines dialer.v1 (see Contracts), but the REST API currently speaks its own snake_case JSON with lowercase dispositions (answered_human, …), not protojson with enum names (DISPOSITION_ANSWERED_HUMAN). Alignment work (the CDR item is delivered in core main; webhooks and the rest of the REST surface keep their own shape):

  • Shipped in core v0.87.0 (core#1472), and in the API Reference: GET /v1/reports/cdr, the CDR as canonical dialer.v1.CallDetailRecord protojson (camelCase, enum names, RFC 3339 timestamps, proto3 defaults omitted), with its field and enum names checked by core's tests against the .proto at the pinned contracts tag — see GET /v1/reports/cdr. It answers the same filters, keyset pagination and tenant isolation as /v1/call_attempts, which keeps its snake_case shape. One row of the append-only call_attempts table is one record, so a correction is its own record; a disposition outside the contract's closed enum travels in labels.disposition_code.
  • Shipped in core v0.86.0 (published as v0.86.0-prod-0.1), and in the API Reference: GET /v1/dispositions, the catalog of the dialer.v1.Disposition enum with each lowercase REST value, its enum name, its enum number and a description. It lists the contract's closed set and leaves out unspecified; call_attempts.disposition stays an open vocabulary and can carry codes that are not in the catalog (abandoned, delivered, ai_conversation, …).
  • Shipped: the mapping of the engine's lowercase dispositions onto the Protobuf enum naming is one table in code, derived from the enum mirror that CI compares with the pinned contracts tag; a new enum value without its REST pair turns CI red, and a source check keeps the contract values from being typed as loose strings elsewhere. No third shape was invented: REST keeps the lowercase value (as in the CDR query) and the catalog carries the enum name beside it.

Compliance reporting endpoints ​

  • Collection-notice attestation is shipped in core v0.88.0 (core#1587, ADR #211): the agent appends what they declared for one call, GET /v1/call_attempts and the attempts CSV expose one of five states, and defense packet version 2 carries every declaration. The off-by-default, founder-owned enforcement key can require the declaration before a contacted debt call closes. See Collection-notice attestation.
  • Durable blocked-dial reporting shipped under different paths: GET /v1/stats/blocked (by-gate counts + daily series over the append-only gate_blocks evidence) and the packaged GET /v1/reports/violations-prevented (JSON/CSV). The proposed GET /v1/reports/compliance/blocked name is superseded by these.
  • Shipped in core v0.86.0 (published as v0.86.0-prod-0.1), and in the API Reference: GET /v1/consumers/{id}, the debtor card. The consumer is identified by the consumer_ref your CMS sent on import (the platform has no separate consumers record; it is the key Reg F already counts under). The card carries the consumer's debts, each with its Reg F counter and its own next permitted window, the cease-and-desist records, DNC counts per list (never the numbers) and the next window for the consumer as a whole in UTC and in the destination's local time. Everything is computed when you ask, from the append-only call, conversation, cease-and-desist and DNC records, by running the same quiet-hours, state-matrix and Reg F checks that gate a dial; not_evaluated_gates names what the window does not cover (consent, AI and artificial-voice disclosure, campaign state, DNC freshness). When the card cannot decide it says undecidable and why; it never answers "call now" by default. The sealed GET /v1/debts/{id}/defense-packet remains the litigation bundle.
  • Defense-packet archives are shipped in core v0.88.0 (core#1578): an explicit POST archives the ZIP with store-reported retention and an RFC 3161 time-stamp; the two GET operations list attestations and return the offline verification kit. The integration guide distinguishes COMPLIANCE/GOVERNANCE object retention from bucket_lock configured retention and walks a third party through openssl ts -verify. (Contract: the archiveDefensePacket, listDefensePacketArchives and getDefensePacketAttestation operations; archive fields: the DefensePacketArchive schema.)

User accounts, roles, SSO, MFA ​

  • Dashboard user accounts — shipped: email + password login minting a revocable ddu_ session, logout, password reset, and user management (create, disable, re-enable — rows are audit evidence and never delete). Machine-to-machine callers still use per-tenant API keys, and there is no X-Tenant-ID impersonation.
  • Roles — shipped, but deliberately small: a two-value role on each user (member / account_admin). Only an account_admin may write the empresa-wide rows sibling sedes inherit; a member gets 403. The role is read-only over the API — promotion is an operator gesture, not a request. There is no general role/permission system.
  • SSO (SAML) — shipped in core v0.86.0 (published as v0.86.0-prod-0.1); its routes are in the API Reference (the contract is described in Authentication). SP-initiated login through the tenant's own IdP, configured by an account_admin, with strict assertion validation, no just-in-time provisioning and a two-step exchange for the ddu_ session. It is inert in a cell that has no SP base URL. Requiring SSO for a tenant is a per-tenant setting (sso_required_enforced) that ships off; turning it on is an enforcement decision. The dashboard's "Sign in with SSO" button and IdP configuration screen ship in dashboard v0.41.0 (dashboard#532, dashboard#520). Single logout is shipped in core v0.88.0 (core#1581): a signed LogoutRequest from the tenant's IdP, posted to /v1/auth/sso/{tenant_id}/slo, revokes every ddu_ session of the person its NameID names (it fails closed with a uniform 400, and an unknown NameID answers exactly like a known one), and POST /v1/auth/logout of an SSO-born session also returns the IdP's logout URL when the IdP published one; the local revocation never depends on the IdP. IdP-initiated login is deliberately not supported: an unsolicited response would be a login-CSRF and replay vector, which the two-step exchange closes, so a sign-in always starts at the platform.
  • MFA (TOTP + recovery codes) — shipped in core v0.86.0 (published as v0.86.0-prod-0.1); its routes are in the API Reference (the contract is described in Authentication). Optional per user: a person enrols an authenticator app (RFC 6238 TOTP) and gets ten single-use recovery codes, shown once; with a factor active, POST /v1/auth/login returns a one-shot challenge instead of a session and the ddu_ token is minted only at POST /v1/auth/mfa/verify. Tenant API keys (dd_) are not affected. Requiring a second factor for every user of a tenant is a per-tenant setting (mfa_required_enforced) that ships off; turning it on is an enforcement decision. The administrative recovery path is shipped in core v0.88.0 (core#1580): an account_admin can remove another person's factor after recording a reason, which invalidates the recovery codes, revokes the person's live sessions and appends an audit event; a tenant that requires MFA sends the person through forced enrolment on the next login. SMS remains outside the supported factors.

Agent workspace surfaces ​

  • GET /v1/agent/queue — shipped in core v0.86.0 (published as v0.86.0-prod-0.1), and in the API Reference. It returns what the dialer has in front of the seat of the calling token: the seat's presence, the live call, the callbacks that seat scheduled and that have not run yet, and the live inbound offers addressed to it (empty unless the tenant's inbound queue stream is on). It is a read that composes four existing sources, not a queue model of its own, and it carries ids, dates and priority only: no phone numbers and no notes. It needs a login session (ddu_) bound to an agent seat; a tenant API key (dd_) has no seat and gets 403, and a widget token (ddw_) is outside the route allowlist. There is no per-agent preview queue to show (in preview mode the account is chosen when the call is dialed, and no row reserves it) and callbacks are not assigned to agents (a callback's agent is its author), so neither appears.
  • PATCH /v1/agents/{id}/status — shipped in the same release as above. It is a façade over the existing agent state machine, not a second presence model: {"status": "available" | "paused" | "offline"} is translated to the existing verbs (unpause or end of wrap-up, pause with a required reason from the closed set manual, break, lunch, training, meeting, and checkout). A transition the state machine does not admit answers 409 with the seat's current state in error.current_state; offline → available is one of them, because checkin stays the verb that decides capacity and SIP registration. Any other status, or paused without a valid reason, is 400, an agent of another tenant is 404, and a widget token is 403. Presence also changes via checkin/checkout and the dialer itself.
  • A wrap_up presence state — shipped in core: an answered call can move an agent into wrap_up when the ACW cap is positive, and POST /v1/agents/{id}/wrap-up explicitly closes that ACW state. The governed agent_wrap_up_seconds setting defaults to 0, so ACW is available but off by default; enablement is a platform-operator decision. See core ADR #101, the agent endpoints, and core v0.85.0. The capability remains in core@main.
  • Voice inside the agent widget — built, and armed per environment by the platform operator. The frame registers a softphone of its own with a SIP credential minted for that one agent session; where the lane is not armed the call keeps ringing on the seat device and the frame says so. Your <iframe> needs allow="microphone; autoplay" either way — see Agent widget (embed).
  • A dialpad (typing a number) inside the widget — shipped in core v0.86.0 (published as v0.86.0-prod-0.1) and in the reference frame, and in the API Reference. POST /v1/dial/manual takes an E.164 number the agent typed and resolves it against the phones of the tenant's own accounts: exactly one account → the call goes through the same compliance gates as a preview dial and is attributed to that account; none, several, no campaign or a quarantined number → 422 and no call. A number with no account is not dialable from the widget. What it does, and its outcomes, are in Dialing a typed number (dialpad). Preview dial (POST /v1/campaigns/{id}/dial, on a call row that already carries a campaign and an account) keeps working unchanged.

Telephony control & AMD ​

  • Supervisor audio (listen / whisper / barge / take-over) — shipped (telephony control v0.4): the supervision endpoints are live, the Live Monitor drives them, and supervision.started/ended stream on the live floor.
  • AMD (answering-machine detection) — shipped for campaigns created with amd_enabled. When the call is answered, the platform listens for a voicemail beep for 5 seconds. A beep inside that window is a machine verdict, and the call closes as answered_machine, with amd_verdict: "machine" on its row in GET /v1/call_attempts; see answered calls with no bridge. If no beep is heard in the window, the verdict is human. That bias is on purpose: a mailbox that beeps later is treated as a person, rather than risk hanging up on one.
    • A machine is not a delivery, and it is not dialled again the same day. If no active retry rule of yours for answered_machine applies to the call, the platform applies a default one: the account is not offered again until 08:00 of the next day in the consumer's local time (in every timezone its numbers map to), so at most one voicemail per account per day. Without it, a campaign kept calling the number until the Reg F 7-in-7 limit blocked it (measured on 2026-10-01: 7 calls in 20 seconds). A retry rule of your own for answered_machine, for your whole account or for the call's campaign, replaces the default; the recommended cadences include one that reschedules a machine for 24 hours later.
    • The amd_verdicts rewind filter matches the latest placed attempt and accepts unspecified | human | machine | not_sure; an unknown value returns 422 fail-closed. New verdicts also appear as call.amd_verdict on the live event stream. It was merged in core#1458 and shipped in core v0.86.0 (published as v0.86.0-prod-0.1).
  • Recordings retrieval API — shipped: listing, per-recording fetch, signed playback URLs and legal hold are live.
  • Per-day schedule grid for campaigns — shipped in core: campaigns support a nullable weekly schedule (mon…sun) that only narrows the campaign-wide and quiet-hours windows; an absent schedule preserves the previous behavior, and malformed or empty schedules fail closed. The field is accepted and returned by the campaign API, with 422 for an unreadable or out-of-day window. See core ADR #163, the campaign API, and core v0.85.0. It remains in core@main.
  • Campaign calendar start/end — shipped in core v0.87.0: nullable starts_at and ends_at are wall-clock values in the campaign's default_timezone; the off-by-default campaign_window_enabled operational flag gates the scheduler that starts eligible voice campaigns and stops them at ends_at. See Calendar start and end and core ADR #206.
  • Inbound calls on a company-wide BYOC trunk — shipped in core v0.88.0 (core#1565): scope: "empresa" accepts source_ips, the source identifies the account and the dialed DID selects the destination office. Registration and revoke are restricted to account_admin; each source address remains unique across the platform. See Inbound calls on a company-wide BYOC trunk and core ADR #203.

Import & data plumbing ​

  • CSV multipart upload for /v1/debts/import — shipped in core v0.86.0 (published as v0.86.0-prod-0.1), and in the API Reference. The form accepts an in-memory text/csv file, an explicit target-to-header mapping and comma / semicolon / tab; UTF-8 (optional BOM), default limits of 4 MB and 10,000 rows (per-tenant settings) fail closed before import. Both forms enter the same per-row validation and normalization pipeline.
  • Reg F migration seeds on /v1/debts/import — last_conversation_at shipped in core v0.90.0 (core#1633, ADR #212; JSON since core#1626, CSV since core#1631), and in the API Reference. A migrated portfolio can declare both its prior attempts (prior_attempts_7d, ADR #99) and its last conversation, so the Reg F 7-in-7 counter and the 7-day post-contact cooldown start from the client's real history instead of zero. See Reg F migration seeds.
  • Server-side mapping templates for the CSV mapper — shipped in core v0.86.0 (published as v0.86.0-prod-0.1; core#1461): /v1/import-mappings, per-tenant and append-only. Editing the mapping or options appends a new version and never rewrites the one an import used; templates are archived, never deleted; and an import that names a template with ?mapping_id= (on POST /v1/debts/import or POST /v1/debt-lists/{id}/import) records the version it used (GET /v1/import-mappings/{id}/uses). The dashboard reads and saves templates from the server since dashboard v0.41.0 (dashboard#513) and offers to move the ones kept in the browser once. See Import mapping templates.
  • Per-key request quotas on /v1 — shipped in core v0.86.0 (published as v0.86.0-prod-0.1) (core#1471, ADR #193), and in the API Reference. Each dd_ key has its own per-minute budget, every counted response carries X-RateLimit-*, and a 429 rate_limited carries Retry-After; the fixed-window brakes (sign-in, widget tokens, call control, dispositions, invitations) gain Retry-After too. The dial/SMS rate brakes and the monthly spend caps (429 throttled / 429 sms_spend_cap_exceeded) deliberately do not: the platform does not know when their capacity returns. See Rate limits & caps.

Not served. The jurisdiction-by-jurisdiction legal matrix behind recording retention, the price of retention above the floor, and the legal text of the screen-monitoring notice wait on a decision tracked in macro-tasks#415. What core serves today is the contract only: the floors and sources returned by GET /v1/business-types, which cite US federal rules and not state law, and the notice and acknowledgement routes described in Recording transcripts, redaction and the screen notice. Do not read either as a statement of what the law requires.

Channels ​

Voice, SMS, and email are live in core. Email shipped in core v0.97.0 with provenance-bound addresses, append-only suppression, approved templates, disposition-triggered sends, email_blast, signed provider events, and one-click unsubscribe; it remains off by default per tenant. See the email channel guide. SMS has gated one-off sends, a unified ledger, per-region provider routing, and signed provider webhooks — see the SMS reference and Webhooks. WhatsApp is not built.

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