Appearance
Email
Generated from
DialerDigital/corecommit0fc18008b3c8— specopenapi/dialer-v1.yamlv0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.
Debtor email addresses with provenance, the suppression ledger (read) and the email ledger (read).
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/email/addresses | The email addresses of one contact |
POST | /v1/email/addresses | Record a debtor email address WITH its provenance |
GET | /v1/email/addresses/{id} | One email address with its read-time eligibility |
GET | /v1/email/suppressions | The suppression history of one address (read only) |
GET | /v1/email/messages | Email ledger query (read only) |
GET | /v1/email/messages/{id} | Collapsed single email view |
The email addresses of one contact
http
GET /v1/email/addressesParameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
contact_id | query | string (uuid) | yes |
Responses
200
The contact's addresses, oldest first, each with its read-time eligibility.
| Field | Type | Required | Description |
|---|---|---|---|
email_addresses | array of EmailAddress | yes | |
email_addresses[].id | string (uuid) | yes | |
email_addresses[].contact_id | string (uuid) | yes | |
email_addresses[].address | string | yes | |
email_addresses[].provenance | consumer_replied · consumer_consent · creditor_notice · prior_collector · imported_unverified | yes | |
email_addresses[].provenance_evidence | string | no | |
email_addresses[].notice_sent_at | string (date-time) | null | no | |
email_addresses[].obtained_at | string (date-time) | no | |
email_addresses[].domain_public | boolean | null | no | Derived from the address domain; never client input. |
email_addresses[].sendable | boolean | yes | |
email_addresses[].ineligible_reason | provenance_not_sendable · evidence_missing · notice_date_missing · notice_period_open · domain_not_public · suppressed · suppression_unreadable | null | yes | |
email_addresses[].created_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Record a debtor email address WITH its provenance
http
POST /v1/email/addressesStores one address of a contact (contact_id) with the provenance that makes it usable or not (12 CFR 1006.6(d)(4)): consumer_replied, consumer_consent, creditor_notice (needs notice_sent_at; sendable only 35 days after it and for a public mail domain) or prior_collector need non-blank provenance_evidence; imported_unverified is stored but NEVER sendable. sendable / ineligible_reason in the answer are computed at read time (provenance, notice period, suppression ledger) — no field of the request can declare an address sendable. The address is normalized (trimmed, lower-cased); recording the same address twice for a contact is 409, an unknown contact 400. Needs the tenant scope (a dd_ key or a dashboard login).
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
contact_id | string (uuid) | yes | |
address | string (email) | yes | |
provenance | consumer_replied · consumer_consent · creditor_notice · prior_collector · imported_unverified | yes | |
provenance_evidence | string | no | Required (non-blank) for every provenance except imported_unverified. |
notice_sent_at | string (date-time) | no | Required for creditor_notice. |
obtained_at | string (date-time) | yes |
json
{
"contact_id": "00000000-0000-4000-8000-000000000001",
"address": "[email protected]",
"provenance": "consumer_consent",
"provenance_evidence": "Written consent recorded in the account file",
"obtained_at": "2026-10-01T12:00:00Z"
}Responses
201
The recorded address.
| Field | Type | Required | Description |
|---|---|---|---|
email_address | object (EmailAddress) | yes | A debtor email address with its provenance. sendable is computed when read, never stored. |
email_address.id | string (uuid) | yes | |
email_address.contact_id | string (uuid) | yes | |
email_address.address | string | yes | |
email_address.provenance | consumer_replied · consumer_consent · creditor_notice · prior_collector · imported_unverified | yes | |
email_address.provenance_evidence | string | no | |
email_address.notice_sent_at | string (date-time) | null | no | |
email_address.obtained_at | string (date-time) | no | |
email_address.domain_public | boolean | null | no | Derived from the address domain; never client input. |
email_address.sendable | boolean | yes | |
email_address.ineligible_reason | provenance_not_sendable · evidence_missing · notice_date_missing · notice_period_open · domain_not_public · suppressed · suppression_unreadable | null | yes | |
email_address.created_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}409 — Conflict
Invalid state transition, uniqueness conflict, or a seat that is busy/reserved.
json
{
"error": {
"code": "conflict",
"message": "invalid state transition"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}One email address with its read-time eligibility
http
GET /v1/email/addresses/{id}Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The address.
| Field | Type | Required | Description |
|---|---|---|---|
email_address | object (EmailAddress) | yes | A debtor email address with its provenance. sendable is computed when read, never stored. |
email_address.id | string (uuid) | yes | |
email_address.contact_id | string (uuid) | yes | |
email_address.address | string | yes | |
email_address.provenance | consumer_replied · consumer_consent · creditor_notice · prior_collector · imported_unverified | yes | |
email_address.provenance_evidence | string | no | |
email_address.notice_sent_at | string (date-time) | null | no | |
email_address.obtained_at | string (date-time) | no | |
email_address.domain_public | boolean | null | no | Derived from the address domain; never client input. |
email_address.sendable | boolean | yes | |
email_address.ineligible_reason | provenance_not_sendable · evidence_missing · notice_date_missing · notice_period_open · domain_not_public · suppressed · suppression_unreadable | null | yes | |
email_address.created_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}The suppression history of one address (read only)
http
GET /v1/email/suppressionsThe append-only suppression ledger of ONE address, newest first, plus the resulting status («the last row wins»: suppressed when the newest row is a suppress, else clear). There is no write route: suppressions are written by the unsubscribe / bounce / complaint paths, and a lift needs the consumer's own provenance. The address digest is never returned. A malformed address is 400.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
address | query | string (email) | yes |
Responses
200
The address's ledger rows (at most 100) and its current status.
| Field | Type | Required | Description |
|---|---|---|---|
status | suppressed · clear | yes | |
email_suppressions | array of EmailSuppression | yes | |
email_suppressions[].id | string (uuid) | no | |
email_suppressions[].seq | integer | no | Position in the address's ledger; the highest wins. |
email_suppressions[].action | suppress · lift | no | |
email_suppressions[].reason | unsubscribe · hard_bounce · complaint · third_party · cease_communication | null | no | Set on suppress rows. |
email_suppressions[].provenance | consumer_consent · consumer_replied | null | no | Set on lift rows. |
email_suppressions[].evidence | string | no | |
email_suppressions[].occurred_at | string (date-time) | no | |
email_suppressions[].recorded_at | string (date-time) | no |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Email ledger query (read only)
http
GET /v1/email/messagesLedger ROWS — originals AND status transitions (corrects_id distinguishes) — newest first, keyset-paginated like /v1/sms/messages. Bodies are 40-char previews in lists. Sending is not part of this surface: it goes through the gated Sender (campaign send_email), never a route.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
direction | query | outbound · inbound | no | |
status | query | queued · sent · delivered · deferred · bounced · complained · failed · received | no | |
campaign_id | query | string (uuid) | no | |
debt_id | query | string (uuid) | no | |
limit | query | integer | no | Page size (default 100, max 1000). |
cursor | query | string | no | Opaque keyset cursor from the previous page's next_cursor. |
Responses
200
One page of ledger rows.
| Field | Type | Required | Description |
|---|---|---|---|
email_messages | array of EmailMessage | yes | |
email_messages[].id | string (uuid) | no | |
email_messages[].direction | outbound · inbound | no | |
email_messages[].status | queued · sent · delivered · deferred · bounced · complained · failed · received | no | |
email_messages[].from_address | string | null | no | |
email_messages[].to_address | string | no | |
email_messages[].subject | string | null | no | |
email_messages[].body_preview | string | null | no | |
email_messages[].template_version_id | string (uuid) | null | no | |
email_messages[].region | string | no | |
email_messages[].client_ref | string | no | |
email_messages[].provider_id | string (uuid) | null | no | |
email_messages[].provider_message_id | string | no | |
email_messages[].campaign_id | string (uuid) | null | no | |
email_messages[].debt_id | string (uuid) | null | no | |
email_messages[].contact_id | string (uuid) | null | no | |
email_messages[].occurred_at | string (date-time) | no | |
email_messages[].corrects_id | string (uuid) | null | no | |
email_messages[].recorded_at | string (date-time) | no | |
next_cursor | string | null | yes |
400 — BadRequest
Malformed request (shape/type errors, invalid filters, bad timestamps).
json
{
"error": {
"code": "bad_request",
"message": "from must be RFC-3339"
}
}401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}Collapsed single email view
http
GET /v1/email/messages/{id}The original's identity + CURRENT status, per-status timestamps, frozen compliance_snapshot, full subject/body and the ordered transitions rows.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The collapsed view.
| Field | Type | Required | Description |
|---|---|---|---|
email_message | object (EmailMessageView) | yes | The COLLAPSED single-email view — current status, per-status timestamps, frozen compliance snapshot, FULL subject/body, ordered transitions. |
email_message.id | string (uuid) | no | |
email_message.direction | outbound · inbound | no | |
email_message.status | queued · sent · delivered · deferred · bounced · complained · failed · received | no | |
email_message.from_address | string | null | no | |
email_message.to_address | string | no | |
email_message.subject | string | null | no | |
email_message.body | string | null | no | |
email_message.template_version_id | string (uuid) | null | no | |
email_message.region | string | no | |
email_message.client_ref | string | no | |
email_message.provider_id | string (uuid) | null | no | |
email_message.provider_message_id | string | no | |
email_message.campaign_id | string (uuid) | null | no | |
email_message.debt_id | string (uuid) | null | no | |
email_message.contact_id | string (uuid) | null | no | |
email_message.compliance_snapshot | object | null | no | |
email_message.occurred_at | string (date-time) | no | |
email_message.timestamps | object | no | Per-status timestamps (e.g. queued, sent, delivered). |
email_message.transitions | array of EmailMessage | no | |
email_message.transitions[].id | string (uuid) | no | |
email_message.transitions[].direction | outbound · inbound | no | |
email_message.transitions[].status | queued · sent · delivered · deferred · bounced · complained · failed · received | no | |
email_message.transitions[].from_address | string | null | no | |
email_message.transitions[].to_address | string | no | |
email_message.transitions[].subject | string | null | no | |
email_message.transitions[].body_preview | string | null | no | |
email_message.transitions[].template_version_id | string (uuid) | null | no | |
email_message.transitions[].region | string | no | |
email_message.transitions[].client_ref | string | no | |
email_message.transitions[].provider_id | string (uuid) | null | no | |
email_message.transitions[].provider_message_id | string | no | |
email_message.transitions[].campaign_id | string (uuid) | null | no | |
email_message.transitions[].debt_id | string (uuid) | null | no | |
email_message.transitions[].contact_id | string (uuid) | null | no | |
email_message.transitions[].occurred_at | string (date-time) | no | |
email_message.transitions[].corrects_id | string (uuid) | null | no | |
email_message.transitions[].recorded_at | string (date-time) | no |
401 — Unauthorized
Missing, unknown or revoked API key (or, on /v1/admin/*, a bad/unset admin token; on webhooks, a bad/missing signature). A dashboard session (ddu_) that a newer login of the same person ended answers the code session_superseded instead (tenant flag single_session_enforced), so the screen can say why it was signed out; a logout, an expired or an unknown token stay unauthorized.
json
{
"error": {
"code": "unauthorized",
"message": "invalid or revoked API key"
}
}403 — Forbidden
The tenant is suspended. A dashboard session (ddu_) of a tenant whose login policy (ADR 227) is in mode enforce answers 403 with the code login_policy_denied on a request from an address or at an hour the policy does not allow — the same body for both; the session is not revoked and a later request inside the policy goes through. A widget token (ddw_) of a seat is held to the same policy on every request (core#2406, flag widget_login_honors_policy). A synthetic (demo) tenant answers 403 with the code synthetic_tenant on every route that would reach a third party — manual dial, external transfer or conference leg, SMS send (core#2251, ADR #280 addendum). No payment changes it.
json
{
"error": {
"code": "forbidden",
"message": "tenant is suspended"
}
}404 — NotFound
Unknown id, malformed (non-UUID) id OR another tenant's id — RLS returns zero rows, so all three are indistinguishable by design (no existence leak, never a 403 for foreign ids).
json
{
"error": {
"code": "not_found",
"message": "resource not found for this tenant"
}
}429 — ApiKeyRateLimited
This API key is over its request quota, or the tenant's keys together are over the tenant ceiling (core#2503) (code rate_limited). The request was not processed. Wait Retry-After seconds and retry; a refusal is not counted against the budget, so that wait is always enough. Bounded per API key and, above the keys, per tenant (api_tenant_requests_per_minute): a sibling key is unaffected until the tenant's keys together reach that ceiling. The quota applies to every operation reachable with a dd_ key, so the operations that name a more specific 429 (call control, dispositions, SMS, …) can ALSO answer this one with its X-RateLimit-* headers.
json
{
"error": {
"code": "rate_limited",
"message": "API key request quota exceeded; retry after the interval in Retry-After"
}
}