Skip to content

Sms templates ​

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

Per-tenant SMS templates with typed variables, approval and the "templates only" setting.

Endpoints ​

MethodPathSummary
GET/v1/sms-templatesSMS template versions
POST/v1/sms-templatesCreate a draft (the next version when name already exists)
GET/v1/sms-templates/{id}One template version
PATCH/v1/sms-templates/{id}Edit a draft (body / variables)
POST/v1/sms-templates/{id}/approveApprove a draft (account_admin only)
POST/v1/sms-templates/{id}/retireRetire a version (a draft is discarded by retiring it)
GET/v1/sms/template-policyThe tenant's "templates only" setting
PUT/v1/sms/template-policyTurn "templates only" on or off (account_admin only)

SMS template versions ​

http
GET /v1/sms-templates

Every version of every template of the tenant, newest version first per name.

Parameters

NameInTypeRequiredDescription
statusquerydraft · approved · retiredno
namequerystringno

Responses

200 ​

The tenant's template versions.

FieldTypeRequiredDescription
sms_templatesarray of SmsTemplateyes
sms_templates[].idstring (uuid)no
sms_templates[].namestringno
sms_templates[].versionintegerno
sms_templates[].statusdraft · approved · retiredno
sms_templates[].bodystringno
sms_templates[].variablesarray of SmsTemplateVariableno
sms_templates[].variables[].namestringyes
sms_templates[].variables[].typestring · number · dateyes
sms_templates[].variables[].requiredbooleannoAn optional variable that is not sent renders empty.
sms_templates[].approved_bystring (uuid) | nullno
sms_templates[].approved_atstring (date-time) | nullno
sms_templates[].retired_atstring (date-time) | nullno
sms_templates[].created_atstring (date-time)no
sms_templates[].updated_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"
  }
}

Create a draft (the next version when name already exists) ​

http
POST /v1/sms-templates

The body uses {{variable}} markers; every marker must be declared in variables and every declared variable used. The static text must already carry the opt-out notice and no carrier-prohibited term (the same rules the send enforces). Variable types: string (1-160 chars, no control characters), number, date (ISO-8601). 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)

FieldTypeRequiredDescription
namestringyesTemplate family; an existing name opens its next version.
bodystringyes
variablesarray of SmsTemplateVariableno
variables[].namestringyes
variables[].typestring · number · dateyes
variables[].requiredbooleannoAn optional variable that is not sent renders empty.
json
{
  "name": "payment-reminder",
  "body": "Hi {{first_name}}, your balance of {{amount}} is due {{due_date}}. Reply STOP to opt out.",
  "variables": [
    {
      "name": "first_name",
      "type": "string"
    },
    {
      "name": "amount",
      "type": "number"
    },
    {
      "name": "due_date",
      "type": "date"
    }
  ]
}

Responses

201 ​

The new draft.

FieldTypeRequiredDescription
sms_templateobject (SmsTemplate)yes
sms_template.idstring (uuid)no
sms_template.namestringno
sms_template.versionintegerno
sms_template.statusdraft · approved · retiredno
sms_template.bodystringno
sms_template.variablesarray of SmsTemplateVariableno
sms_template.variables[].namestringyes
sms_template.variables[].typestring · number · dateyes
sms_template.variables[].requiredbooleannoAn optional variable that is not sent renders empty.
sms_template.approved_bystring (uuid) | nullno
sms_template.approved_atstring (date-time) | nullno
sms_template.retired_atstring (date-time) | nullno
sms_template.created_atstring (date-time)no
sms_template.updated_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 template version ​

http
GET /v1/sms-templates/{id}

Parameters

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

Responses

200 ​

The version.

FieldTypeRequiredDescription
sms_templateobject (SmsTemplate)yes
sms_template.idstring (uuid)no
sms_template.namestringno
sms_template.versionintegerno
sms_template.statusdraft · approved · retiredno
sms_template.bodystringno
sms_template.variablesarray of SmsTemplateVariableno
sms_template.variables[].namestringyes
sms_template.variables[].typestring · number · dateyes
sms_template.variables[].requiredbooleannoAn optional variable that is not sent renders empty.
sms_template.approved_bystring (uuid) | nullno
sms_template.approved_atstring (date-time) | nullno
sms_template.retired_atstring (date-time) | nullno
sms_template.created_atstring (date-time)no
sms_template.updated_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"
  }
}

Edit a draft (body / variables) ​

http
PATCH /v1/sms-templates/{id}

Only a draft can change; an approved or retired version answers 409 (version it instead).

Parameters

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

Request body (JSON, required)

FieldTypeRequiredDescription
bodystringno
variablesarray of SmsTemplateVariableno
variables[].namestringyes
variables[].typestring · number · dateyes
variables[].requiredbooleannoAn optional variable that is not sent renders empty.

Responses

200 ​

The updated draft.

FieldTypeRequiredDescription
sms_templateobject (SmsTemplate)yes
sms_template.idstring (uuid)no
sms_template.namestringno
sms_template.versionintegerno
sms_template.statusdraft · approved · retiredno
sms_template.bodystringno
sms_template.variablesarray of SmsTemplateVariableno
sms_template.variables[].namestringyes
sms_template.variables[].typestring · number · dateyes
sms_template.variables[].requiredbooleannoAn optional variable that is not sent renders empty.
sms_template.approved_bystring (uuid) | nullno
sms_template.approved_atstring (date-time) | nullno
sms_template.retired_atstring (date-time) | nullno
sms_template.created_atstring (date-time)no
sms_template.updated_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"
  }
}

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/sms-templates/{id}/approve

Approves 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

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

Responses

200 ​

The approved version.

FieldTypeRequiredDescription
sms_templateobject (SmsTemplate)yes
sms_template.idstring (uuid)no
sms_template.namestringno
sms_template.versionintegerno
sms_template.statusdraft · approved · retiredno
sms_template.bodystringno
sms_template.variablesarray of SmsTemplateVariableno
sms_template.variables[].namestringyes
sms_template.variables[].typestring · number · dateyes
sms_template.variables[].requiredbooleannoAn optional variable that is not sent renders empty.
sms_template.approved_bystring (uuid) | nullno
sms_template.approved_atstring (date-time) | nullno
sms_template.retired_atstring (date-time) | nullno
sms_template.created_atstring (date-time)no
sms_template.updated_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"
  }
}

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 (a draft is discarded by retiring it) ​

http
POST /v1/sms-templates/{id}/retire

Terminal. A retired version can no longer be sent; an already retired one answers 409.

Parameters

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

Responses

200 ​

The retired version.

FieldTypeRequiredDescription
sms_templateobject (SmsTemplate)yes
sms_template.idstring (uuid)no
sms_template.namestringno
sms_template.versionintegerno
sms_template.statusdraft · approved · retiredno
sms_template.bodystringno
sms_template.variablesarray of SmsTemplateVariableno
sms_template.variables[].namestringyes
sms_template.variables[].typestring · number · dateyes
sms_template.variables[].requiredbooleannoAn optional variable that is not sent renders empty.
sms_template.approved_bystring (uuid) | nullno
sms_template.approved_atstring (date-time) | nullno
sms_template.retired_atstring (date-time) | nullno
sms_template.created_atstring (date-time)no
sms_template.updated_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"
  }
}

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"
  }
}

The tenant's "templates only" setting ​

http
GET /v1/sms/template-policy

Off (false) until an account_admin turns it on.

Responses

200 ​

The current setting.

FieldTypeRequiredDescription
sms_template_policyobject (SmsTemplatePolicy)yes
sms_template_policy.templates_onlybooleanyes

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"
  }
}

Turn "templates only" on or off (account_admin only) ​

http
PUT /v1/sms/template-policy

With it on, POST /v1/sms/messages refuses free text (422 sms_templates_only) and only approved templates send. Each change is an append-only ledger row; a member or a dd_ machine key gets 403. Campaign SMS (sms_body) is not covered by this setting.

Request body (JSON, required)

FieldTypeRequiredDescription
templates_onlybooleanyes
json
{
  "templates_only": true
}

Responses

200 ​

The stored setting.

FieldTypeRequiredDescription
sms_template_policyobject (SmsTemplatePolicy)yes
sms_template_policy.templates_onlybooleanyes

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"
  }
}

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