Skip to content

Email ​

Generated from DialerDigital/core commit 0fc18008b3c8 — spec openapi/dialer-v1.yaml v0.134.0. Do not edit by hand; see How this reference is built. Download the OpenAPI spec.

Español

Debtor email addresses with provenance, the suppression ledger (read) and the email ledger (read).

Endpoints ​

MethodPathSummary
GET/v1/email/addressesThe email addresses of one contact
POST/v1/email/addressesRecord a debtor email address WITH its provenance
GET/v1/email/addresses/{id}One email address with its read-time eligibility
GET/v1/email/suppressionsThe suppression history of one address (read only)
GET/v1/email/messagesEmail ledger query (read only)
GET/v1/email/messages/{id}Collapsed single email view

The email addresses of one contact ​

http
GET /v1/email/addresses

Parameters

NameInTypeRequiredDescription
contact_idquerystring (uuid)yes

Responses

200 ​

The contact's addresses, oldest first, each with its read-time eligibility.

FieldTypeRequiredDescription
email_addressesarray of EmailAddressyes
email_addresses[].idstring (uuid)yes
email_addresses[].contact_idstring (uuid)yes
email_addresses[].addressstringyes
email_addresses[].provenanceconsumer_replied · consumer_consent · creditor_notice · prior_collector · imported_unverifiedyes
email_addresses[].provenance_evidencestringno
email_addresses[].notice_sent_atstring (date-time) | nullno
email_addresses[].obtained_atstring (date-time)no
email_addresses[].domain_publicboolean | nullnoDerived from the address domain; never client input.
email_addresses[].sendablebooleanyes
email_addresses[].ineligible_reasonprovenance_not_sendable · evidence_missing · notice_date_missing · notice_period_open · domain_not_public · suppressed · suppression_unreadable | nullyes
email_addresses[].created_atstring (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/addresses

Stores 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)

FieldTypeRequiredDescription
contact_idstring (uuid)yes
addressstring (email)yes
provenanceconsumer_replied · consumer_consent · creditor_notice · prior_collector · imported_unverifiedyes
provenance_evidencestringnoRequired (non-blank) for every provenance except imported_unverified.
notice_sent_atstring (date-time)noRequired for creditor_notice.
obtained_atstring (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.

FieldTypeRequiredDescription
email_addressobject (EmailAddress)yesA debtor email address with its provenance. sendable is computed when read, never stored.
email_address.idstring (uuid)yes
email_address.contact_idstring (uuid)yes
email_address.addressstringyes
email_address.provenanceconsumer_replied · consumer_consent · creditor_notice · prior_collector · imported_unverifiedyes
email_address.provenance_evidencestringno
email_address.notice_sent_atstring (date-time) | nullno
email_address.obtained_atstring (date-time)no
email_address.domain_publicboolean | nullnoDerived from the address domain; never client input.
email_address.sendablebooleanyes
email_address.ineligible_reasonprovenance_not_sendable · evidence_missing · notice_date_missing · notice_period_open · domain_not_public · suppressed · suppression_unreadable | nullyes
email_address.created_atstring (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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200 ​

The address.

FieldTypeRequiredDescription
email_addressobject (EmailAddress)yesA debtor email address with its provenance. sendable is computed when read, never stored.
email_address.idstring (uuid)yes
email_address.contact_idstring (uuid)yes
email_address.addressstringyes
email_address.provenanceconsumer_replied · consumer_consent · creditor_notice · prior_collector · imported_unverifiedyes
email_address.provenance_evidencestringno
email_address.notice_sent_atstring (date-time) | nullno
email_address.obtained_atstring (date-time)no
email_address.domain_publicboolean | nullnoDerived from the address domain; never client input.
email_address.sendablebooleanyes
email_address.ineligible_reasonprovenance_not_sendable · evidence_missing · notice_date_missing · notice_period_open · domain_not_public · suppressed · suppression_unreadable | nullyes
email_address.created_atstring (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/suppressions

The 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

NameInTypeRequiredDescription
addressquerystring (email)yes

Responses

200 ​

The address's ledger rows (at most 100) and its current status.

FieldTypeRequiredDescription
statussuppressed · clearyes
email_suppressionsarray of EmailSuppressionyes
email_suppressions[].idstring (uuid)no
email_suppressions[].seqintegernoPosition in the address's ledger; the highest wins.
email_suppressions[].actionsuppress · liftno
email_suppressions[].reasonunsubscribe · hard_bounce · complaint · third_party · cease_communication | nullnoSet on suppress rows.
email_suppressions[].provenanceconsumer_consent · consumer_replied | nullnoSet on lift rows.
email_suppressions[].evidencestringno
email_suppressions[].occurred_atstring (date-time)no
email_suppressions[].recorded_atstring (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/messages

Ledger 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

NameInTypeRequiredDescription
directionqueryoutbound · inboundno
statusqueryqueued · sent · delivered · deferred · bounced · complained · failed · receivedno
campaign_idquerystring (uuid)no
debt_idquerystring (uuid)no
limitqueryintegernoPage size (default 100, max 1000).
cursorquerystringnoOpaque keyset cursor from the previous page's next_cursor.

Responses

200 ​

One page of ledger rows.

FieldTypeRequiredDescription
email_messagesarray of EmailMessageyes
email_messages[].idstring (uuid)no
email_messages[].directionoutbound · inboundno
email_messages[].statusqueued · sent · delivered · deferred · bounced · complained · failed · receivedno
email_messages[].from_addressstring | nullno
email_messages[].to_addressstringno
email_messages[].subjectstring | nullno
email_messages[].body_previewstring | nullno
email_messages[].template_version_idstring (uuid) | nullno
email_messages[].regionstringno
email_messages[].client_refstringno
email_messages[].provider_idstring (uuid) | nullno
email_messages[].provider_message_idstringno
email_messages[].campaign_idstring (uuid) | nullno
email_messages[].debt_idstring (uuid) | nullno
email_messages[].contact_idstring (uuid) | nullno
email_messages[].occurred_atstring (date-time)no
email_messages[].corrects_idstring (uuid) | nullno
email_messages[].recorded_atstring (date-time)no
next_cursorstring | nullyes

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

NameInTypeRequiredDescription
idpathstring (uuid)yesResource UUID. Malformed or cross-tenant ids read as 404.

Responses

200 ​

The collapsed view.

FieldTypeRequiredDescription
email_messageobject (EmailMessageView)yesThe COLLAPSED single-email view — current status, per-status timestamps, frozen compliance snapshot, FULL subject/body, ordered transitions.
email_message.idstring (uuid)no
email_message.directionoutbound · inboundno
email_message.statusqueued · sent · delivered · deferred · bounced · complained · failed · receivedno
email_message.from_addressstring | nullno
email_message.to_addressstringno
email_message.subjectstring | nullno
email_message.bodystring | nullno
email_message.template_version_idstring (uuid) | nullno
email_message.regionstringno
email_message.client_refstringno
email_message.provider_idstring (uuid) | nullno
email_message.provider_message_idstringno
email_message.campaign_idstring (uuid) | nullno
email_message.debt_idstring (uuid) | nullno
email_message.contact_idstring (uuid) | nullno
email_message.compliance_snapshotobject | nullno
email_message.occurred_atstring (date-time)no
email_message.timestampsobjectnoPer-status timestamps (e.g. queued, sent, delivered).
email_message.transitionsarray of EmailMessageno
email_message.transitions[].idstring (uuid)no
email_message.transitions[].directionoutbound · inboundno
email_message.transitions[].statusqueued · sent · delivered · deferred · bounced · complained · failed · receivedno
email_message.transitions[].from_addressstring | nullno
email_message.transitions[].to_addressstringno
email_message.transitions[].subjectstring | nullno
email_message.transitions[].body_previewstring | nullno
email_message.transitions[].template_version_idstring (uuid) | nullno
email_message.transitions[].regionstringno
email_message.transitions[].client_refstringno
email_message.transitions[].provider_idstring (uuid) | nullno
email_message.transitions[].provider_message_idstringno
email_message.transitions[].campaign_idstring (uuid) | nullno
email_message.transitions[].debt_idstring (uuid) | nullno
email_message.transitions[].contact_idstring (uuid) | nullno
email_message.transitions[].occurred_atstring (date-time)no
email_message.transitions[].corrects_idstring (uuid) | nullno
email_message.transitions[].recorded_atstring (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"
  }
}

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