Appearance
Email templates
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.
Per-tenant e-mail templates (static subject + body with typed variables), append-only versions, account_admin approval and retirement.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET | /v1/email-templates | E-mail template versions |
POST | /v1/email-templates | Create a draft (the next version when name already exists) |
GET | /v1/email-templates/{id} | One template version |
PATCH | /v1/email-templates/{id} | Edit a draft (subject / body / variables / creditor_names) |
POST | /v1/email-templates/{id}/approve | Approve a draft (account_admin only) |
POST | /v1/email-templates/{id}/retire | Retire a version (account_admin only; a draft is discarded by retiring it) |
E-mail template versions
http
GET /v1/email-templatesEvery version of every e-mail template of the tenant, newest version first per name.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
status | query | draft · approved · retired | no | |
name | query | string | no |
Responses
200
The tenant's template versions.
| Field | Type | Required | Description |
|---|---|---|---|
email_templates | array of EmailTemplate | yes | |
email_templates[].id | string (uuid) | no | |
email_templates[].name | string | no | |
email_templates[].version | integer | no | |
email_templates[].status | draft · approved · retired | no | |
email_templates[].subject | string | no | |
email_templates[].body | string | no | |
email_templates[].variables | array of EmailTemplateVariable | no | |
email_templates[].variables[].name | string | yes | |
email_templates[].variables[].type | string · number · date | yes | |
email_templates[].variables[].required | boolean | no | An optional variable that is not sent renders empty. |
email_templates[].creditor_names | array of string | no | |
email_templates[].approved_by | string (uuid) | null | no | |
email_templates[].approved_at | string (date-time) | null | no | |
email_templates[].retired_at | string (date-time) | null | no | |
email_templates[].created_at | string (date-time) | no | |
email_templates[].updated_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"
}
}Create a draft (the next version when name already exists)
http
POST /v1/email-templatesThe body uses {{variable}} markers; every marker must be declared in variables and every declared variable used. The subject is STATIC text (no markers) and must pass the subject policy against the declared creditor_names: it may not name the debt, an amount or a creditor. The static body must already carry the unsubscribe notice and the collector declaration (the same rules the send enforces). A name that already has versions opens the next version as a new draft: an approved version is never edited. tenant_id, version and status are never body input.
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Template family; an existing name opens its next version. |
subject | string | yes | Static text: no {{}} markers; it may not name the debt, an amount or a creditor. |
body | string | yes | |
variables | array of EmailTemplateVariable | no | |
variables[].name | string | yes | |
variables[].type | string · number · date | yes | |
variables[].required | boolean | no | An optional variable that is not sent renders empty. |
creditor_names | array of string | no | Creditor names the subject policy checks the subject against (trimmed, de-duplicated). |
json
{
"name": "payment-reminder",
"subject": "Un mensaje importante para usted",
"body": "Hola {{first_name}}. Este mensaje proviene de una agencia de cobranza. Para dejar de recibir correos, responda BAJA.",
"variables": [
{
"name": "first_name",
"type": "string"
}
],
"creditor_names": []
}Responses
201
The new draft.
| Field | Type | Required | Description |
|---|---|---|---|
email_template | object (EmailTemplate) | yes | |
email_template.id | string (uuid) | no | |
email_template.name | string | no | |
email_template.version | integer | no | |
email_template.status | draft · approved · retired | no | |
email_template.subject | string | no | |
email_template.body | string | no | |
email_template.variables | array of EmailTemplateVariable | no | |
email_template.variables[].name | string | yes | |
email_template.variables[].type | string · number · date | yes | |
email_template.variables[].required | boolean | no | An optional variable that is not sent renders empty. |
email_template.creditor_names | array of string | no | |
email_template.approved_by | string (uuid) | null | no | |
email_template.approved_at | string (date-time) | null | no | |
email_template.retired_at | string (date-time) | null | no | |
email_template.created_at | string (date-time) | no | |
email_template.updated_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 template version
http
GET /v1/email-templates/{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 version.
| Field | Type | Required | Description |
|---|---|---|---|
email_template | object (EmailTemplate) | yes | |
email_template.id | string (uuid) | no | |
email_template.name | string | no | |
email_template.version | integer | no | |
email_template.status | draft · approved · retired | no | |
email_template.subject | string | no | |
email_template.body | string | no | |
email_template.variables | array of EmailTemplateVariable | no | |
email_template.variables[].name | string | yes | |
email_template.variables[].type | string · number · date | yes | |
email_template.variables[].required | boolean | no | An optional variable that is not sent renders empty. |
email_template.creditor_names | array of string | no | |
email_template.approved_by | string (uuid) | null | no | |
email_template.approved_at | string (date-time) | null | no | |
email_template.retired_at | string (date-time) | null | no | |
email_template.created_at | string (date-time) | no | |
email_template.updated_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"
}
}Edit a draft (subject / body / variables / creditor_names)
http
PATCH /v1/email-templates/{id}Only a draft can change; an approved or retired version answers 409 (version it instead).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Request body (JSON, required)
| Field | Type | Required | Description |
|---|---|---|---|
subject | string | no | |
body | string | no | |
variables | array of EmailTemplateVariable | no | |
variables[].name | string | yes | |
variables[].type | string · number · date | yes | |
variables[].required | boolean | no | An optional variable that is not sent renders empty. |
creditor_names | array of string | no |
Responses
200
The updated draft.
| Field | Type | Required | Description |
|---|---|---|---|
email_template | object (EmailTemplate) | yes | |
email_template.id | string (uuid) | no | |
email_template.name | string | no | |
email_template.version | integer | no | |
email_template.status | draft · approved · retired | no | |
email_template.subject | string | no | |
email_template.body | string | no | |
email_template.variables | array of EmailTemplateVariable | no | |
email_template.variables[].name | string | yes | |
email_template.variables[].type | string · number · date | yes | |
email_template.variables[].required | boolean | no | An optional variable that is not sent renders empty. |
email_template.creditor_names | array of string | no | |
email_template.approved_by | string (uuid) | null | no | |
email_template.approved_at | string (date-time) | null | no | |
email_template.retired_at | string (date-time) | null | no | |
email_template.created_at | string (date-time) | no | |
email_template.updated_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"
}
}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"
}
}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"
}
}Approve a draft (account_admin only)
http
POST /v1/email-templates/{id}/approveApproves the draft exactly as it stands and records who and when (approved_by, approved_at). Only an account_admin login may approve: a member or a dd_ machine key gets 403. The database enforces it too. A version that is not a draft answers 409.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The approved version.
| Field | Type | Required | Description |
|---|---|---|---|
email_template | object (EmailTemplate) | yes | |
email_template.id | string (uuid) | no | |
email_template.name | string | no | |
email_template.version | integer | no | |
email_template.status | draft · approved · retired | no | |
email_template.subject | string | no | |
email_template.body | string | no | |
email_template.variables | array of EmailTemplateVariable | no | |
email_template.variables[].name | string | yes | |
email_template.variables[].type | string · number · date | yes | |
email_template.variables[].required | boolean | no | An optional variable that is not sent renders empty. |
email_template.creditor_names | array of string | no | |
email_template.approved_by | string (uuid) | null | no | |
email_template.approved_at | string (date-time) | null | no | |
email_template.retired_at | string (date-time) | null | no | |
email_template.created_at | string (date-time) | no | |
email_template.updated_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"
}
}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"
}
}Retire a version (account_admin only; a draft is discarded by retiring it)
http
POST /v1/email-templates/{id}/retireTerminal. A retired version can no longer be sent; an already retired one answers 409. Only an account_admin login may retire (a member or a dd_ machine key gets 403): retiring takes an approved template out of use, so it carries the same authority as approving it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string (uuid) | yes | Resource UUID. Malformed or cross-tenant ids read as 404. |
Responses
200
The retired version.
| Field | Type | Required | Description |
|---|---|---|---|
email_template | object (EmailTemplate) | yes | |
email_template.id | string (uuid) | no | |
email_template.name | string | no | |
email_template.version | integer | no | |
email_template.status | draft · approved · retired | no | |
email_template.subject | string | no | |
email_template.body | string | no | |
email_template.variables | array of EmailTemplateVariable | no | |
email_template.variables[].name | string | yes | |
email_template.variables[].type | string · number · date | yes | |
email_template.variables[].required | boolean | no | An optional variable that is not sent renders empty. |
email_template.creditor_names | array of string | no | |
email_template.approved_by | string (uuid) | null | no | |
email_template.approved_at | string (date-time) | null | no | |
email_template.retired_at | string (date-time) | null | no | |
email_template.created_at | string (date-time) | no | |
email_template.updated_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"
}
}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"
}
}