Skip to content

SMS Messaging: Approved Templates, Conversational Inbox & Auto-Replies ​

Spanish version: Mensajería SMS.

SMS messaging in Dialer Digital provides a compliance-first messaging architecture for US debt collection agencies. The platform supports template-based and free-text SMS sends, a multi-user conversational inbox with append-only thread assignment and read states, and automated out-of-hours responses.

Availability: shipped in core since v0.94.0

The SMS messaging features documented here (templates with approval workflows, conversational inbox threads, and out-of-hours auto-replies) are published in core v0.94.0. The request and response schemas correspond strictly to the OpenAPI specification (openapi/dialer-v1.yaml). Confirm with your platform operator that your environment is running v0.94.0 or higher.


Architectural Pillars & Compliance Guardrails ​

All SMS messaging paths operate under strict, fail-closed compliance guardrails:

  1. No Compliance Gate Bypass: Whether a message is sent directly via POST /v1/sms/messages, from a conversational inbox thread, or automatically as an out-of-hours auto-reply, every send passes through the core compliance engine (Consent, DNC, QuietHours, SmsFrequency, SpendGuard, rate brakes, and spend caps).

    • Fail-Closed Behavior: If a consumer lacks consent, or if sending violates quiet hours or frequency limits, the send is refused or blocked (422 compliance_blocked). When the send carries debt_id and campaign_id, the block also lands a durable gate_blocks row; zero traffic reaches the upstream SMS provider either way.
    • Keyword Exemption: Only system keyword replies (STOP, HELP, START) triggered directly by inbound messages are exempt from pre-send compliance checks per CTIA guidelines. Auto-replies and agent inbox messages are never exempt.
    • One STOP acknowledgment per number and window: the opt-out itself is applied on every STOP, but the confirmation message is sent once per (tenant, consumer number) per window, so a third party looping STOP does not bill you one SMS per message. The window is sms_stop_ack_window_seconds (deployment setting, env SMS_STOP_ACK_WINDOW_SECONDS, default 86400 s); 0 acknowledges every STOP. Backed by lib/dialer/sms/inbound.ex and config/runtime.exs in core.
  2. Template Policy Enforcement: Tenants can enforce strict template usage via PUT /v1/sms/template-policy (templates_only: true). When enabled, free-text sends return 422 sms_templates_only.

  3. Mandatory Opt-Out Notice & Content Policy: All template drafts and out-of-hours auto-reply bodies must explicitly include an opt-out notice (e.g. Reply STOP to opt out) and pass static content checks. Missing notices or carrier-prohibited terms are rejected with 422 on creation.

  4. Append-Only Audited State: Thread assignments, read receipts, template policies, and auto-reply configurations are recorded as append-only event logs or version histories. No message ledger or thread event record is ever mutated or deleted.


1. SMS Templates & Approval Workflow ​

SMS templates allow defining structured messages with variable placeholders (such as , , or ).

Roles and Permissions ​

  • Draft creation & editing: Any valid API key or tenant login (member or account_admin).
  • Approval & Policy enforcement: account_admin only. A member or dd_ machine key attempting to approve a template or toggle templates_only policy receives 403 role_forbidden.

Workflow Lifecycle ​

[ POST /v1/sms-templates ]   ──(Creates draft)──> [ Status: draft ]
                                                         │
                                           ┌─────────────┴─────────────┐
                                           ▼                           ▼
[ POST /v1/sms-templates/{id}/approve ] (account_admin)   [ POST /v1/sms-templates/{id}/retire ]
                                           │                           │
                                           ▼                           ▼
                                  [ Status: approved ]        [ Status: retired ]

Endpoints ​

Create a Template Draft ​

POST /v1/sms-templates

Creates a new template draft. If name already exists, a new draft version is created.

Request Body:

json
{
  "name": "payment-reminder",
  "body": "Hi {{first_name}}, your balance of {{amount}} is due on {{due_date}}. Reply STOP to opt out.",
  "variables": [
    { "name": "first_name", "type": "string" },
    { "name": "amount", "type": "number" },
    { "name": "due_date", "type": "date" }
  ]
}
  • Variables: Declared types must be string (1–160 chars), number, or date (ISO-8601). Every variable used in body must be declared in variables, and vice versa.
  • Validation: Body must contain opt-out notice and no prohibited terms.

List Template Versions ​

GET /v1/sms-templates

Lists template versions, ordered newest version first per name. Supports query filters status (draft, approved, retired) and name.

Get a Single Template Version ​

GET /v1/sms-templates/{id}

Returns the template details for the specified version ID.

Update a Draft ​

PATCH /v1/sms-templates/{id}

Updates body and/or variables of a draft template. Editing an approved or retired template returns 409 conflict (a new version draft must be created instead).

Approve a Template Draft ​

POST /v1/sms-templates/{id}/approve

Requires account_admin. Approves the template draft, recording approved_by and approved_at. Approving a non-draft returns 409 conflict.

Retire a Template Version ​

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

Retires a template version. A retired version can no longer be used for sends. Retiring a draft discards it.

Template Policy ("Templates Only") ​

GET /v1/sms/template-policyPUT /v1/sms/template-policy

Enforces that all SMS sends for the tenant must use approved templates.

  • PUT requires account_admin.

Request Body (PUT):

json
{
  "templates_only": true
}

When templates_only is true, any attempt to send free-text SMS via POST /v1/sms/messages returns 422 sms_templates_only.


2. Conversational SMS Inbox (Threads) ​

The conversational inbox groups SMS messages between a consumer's mobile number and a tenant line into a single thread.

Keyset Window & Derived Read-Model ​

  • Threads are derived on read from the underlying ledger over a sliding 31-day window.
  • Message delivery status transitions do not alter thread positions; only original messages update thread activity.
  • Messages sent before thread support was introduced carry no thread_id and are queried through the flat ledger (GET /v1/sms/messages).

Endpoints ​

List Inbox Threads ​

GET /v1/sms/threads

Returns active SMS threads sorted newest-first by the last original message in the window.

Query Parameters:

ParameterTypeRequiredDescription
fromstring (ISO-8601)noStart of window (default: trailing 31 days).
tostring (ISO-8601)noEnd of window.
unreadbooleannotrue filters to threads with inbound messages received after the last team read event.
limitintegernoPage size (1–200, default 50).
cursorstringnoKeyset pagination cursor.

Each thread row carries assigned_to (the user of its last assign/unassign event, or null); the list has no filter by assignee.

Get Thread Detail & Matched Debts ​

GET /v1/sms/threads/{id}

Returns the thread details and includes debt_ids: an array of up to 200 tenant debt IDs matching the consumer's phone number via blind index. An empty debt_ids array indicates no matching debts exist for that number.

Assign a Thread ​

POST /v1/sms/threads/{id}/assign

Writes an append-only assigned or unassigned event signed by the authenticated dashboard login.

Request Body:

json
{
  "user_id": "0d4f4f9e-1f2a-4b53-9d3c-8a5e2f7b1c10"
}
  • Pass null as user_id to release the assignment.
  • Role Rules:
    • member: May only assign the thread to themselves, or release a thread currently assigned to themselves. Attempting to assign to another user returns 403 role_forbidden.
    • account_admin: May assign the thread to any active user of the tenant.
    • API machine keys (dd_) or agent tokens return 403 role_forbidden (an assignment requires a human login identity).
    • Assigning to a disabled, non-existent, or cross-tenant user returns 422 invalid_assignee.

Mark Thread as Read ​

POST /v1/sms/threads/{id}/read

Appends a read event signed by the calling dashboard login. Clears the unread indicator for the entire tenant team for messages prior to the read timestamp. Requires a human login (ddu_); machine keys receive 403 role_forbidden.

List Messages in a Thread ​

GET /v1/sms/messages?thread_id={id}

Lists messages belonging to the specified thread.

Reply from Thread (Composer Send) ​

POST /v1/sms/messages

Sends an outbound message. If from is omitted, the platform resolves the provider's default_from_e164. If no default line is configured, the send fails with 422 from_required.

The sender must be your own number. Whichever applies (from, or the provider's default_from_e164) has to be an E.164 number and an ACTIVE number of the tenant: otherwise 422 sms_from_invalid (not E.164) or 422 sms_from_not_owned (valid, but not an active number of the account; register it under /v1/dids). With did_ownership_proof on enforce, an active number whose ownership is not verified is refused with 422 did_unverified. Nothing is queued and the provider is not contacted. The same rule applies to default_from_e164 when you register or change a provider (POST/PATCH /v1/sms/providers) and to the campaign sender (section 4). Backed by lib/dialer/api/v1_router.ex (sms_from_error/2) and lib/dialer/sms/ in core.

A destination with no rate is refused. When no sell rate covers the send's region, the send is refused with 422 no_rate, for prepaid and postpaid tenants alike. It is not a payment refusal: topping up changes nothing, and retrying unchanged is refused again until the platform prices that region. Backed by respond_sms_send/4 in lib/dialer/api/v1_router.ex and the core CHANGELOG entry on postpaid no_rate.


3. Out-of-Hours SMS Auto-Replies ​

Tenants can configure automatic replies for inbound SMS received outside established business hours.

Operational Requirements ​

An auto-reply is triggered only when all three conditions are satisfied:

  1. The operator flag sms_auto_reply_enabled is set to true.
  2. Tenant business hours (/v1/tenant/sms-business-hours) are configured.
  3. An auto-reply template/body (/v1/tenant/sms-auto-reply) is saved.

If any condition is missing or disabled, no automatic reply is generated.

Fail-Closed Behavior ​

  • QuietHours & Compliance Gates: Auto-replies pass through Sender.send_sms/2. If an inbound text arrives out-of-hours but the consumer is in a timezone under QuietHours or lacks consent, the auto-reply is not sent and is logged in gate_blocks. Auto-replies are never queued for morning delivery.
  • Cooldown Brakes: Auto-replies enforce a configurable cooldown per thread (cooldown_seconds, default 86,400s / 24 hours). A thread receives at most one auto-reply per cooldown window.

Endpoints ​

Get Business Hours ​

GET /v1/tenant/sms-business-hours

Returns the configured SMS business hours, or null if unconfigured.

Set Business Hours ​

PUT /v1/tenant/sms-business-hours

Requires account_admin or dd_ machine key. Sets the weekly schedule for SMS business hours.

Request Body:

json
{
  "timezone": "America/Chicago",
  "weekly": {
    "mon": { "start": "09:00", "end": "17:00" },
    "tue": { "start": "09:00", "end": "17:00" },
    "wed": { "start": "09:00", "end": "17:00" },
    "thu": { "start": "09:00", "end": "17:00" },
    "fri": { "start": "09:00", "end": "17:00" }
  }
}

Clear Business Hours ​

DELETE /v1/tenant/sms-business-hours

Requires account_admin or dd_ machine key. Removes business hours (disabling auto-replies).

Get Auto-Reply Configuration ​

GET /v1/tenant/sms-auto-reply

Returns the active auto-reply version, or null if never saved.

Save Auto-Reply Version ​

PUT /v1/tenant/sms-auto-reply

Requires account_admin or dd_ machine key. Saves a new version of the auto-reply text and cooldown.

Request Body:

json
{
  "body": "We are currently closed. We will respond during business hours. Reply STOP to opt out.",
  "cooldown_seconds": 86400,
  "language": "en"
}
  • Validation: The body must contain an explicit opt-out notice and no prohibited terms (422). If templates_only policy is enabled, saving free-text returns 422 sms_templates_only.

4. Campaign SMS: why a campaign sends nothing ​

The sender of an SMS campaign (its caller_ids, its caller_id_pool or, failing both, the provider's default_from_e164) follows the same rule as a direct send: it must be an ACTIVE number of the tenant. A campaign with no usable sender does not fail: it ticks and sends nothing. GET /v1/campaigns/{id}/stats (and the campaign.stats frame) explains it in stats.send_unavailable_reason: why the last tick sent nothing, or null when it did not stop there. The closed list, read from openapi/dialer-v1.yaml in core:

ValueMeaning
provider_unresolvedNo SMS provider resolves for the account or site; configure one.
caller_ids_no_active_didNone of the campaign's caller_ids is an ACTIVE number of the account.
caller_id_pool_no_active_didThe campaign's caller_id_pool has no active number.
default_from_invalidThe campaign has no sender of its own and the provider's default_from_e164 is not an E.164 number the account owns.
did_unverifieddid_ownership_proof is enforce and the only active numbers the campaign would send from have no proven ownership.
outside_calling_windowThe tick fell outside the campaign's calling window; nothing is sent until the window opens (not set when calling_window_destination_local is on, where the window is decided per lead).

It is cleared by the first later tick that passes the gates.


HTTP Error Codes & Contract Reference ​

Status CodeError codeCause
400bad_requestMalformed JSON body, window > 31 days in threads query, missing required field (user_id).
401unauthorizedMissing, invalid, or expired Bearer token.
402wallet_insufficientPrepaid balance is too low for this send. Top up the wallet, then retry.
402plan_exhaustedThe SMS allowance included in the plan is used up. Upgrade the plan (or wait for the next period), then retry.
402exposure_limitThe pre-verification exposure limit is reached. Complete the account verification (KYC), then retry.
403role_forbiddenNon-admin user attempting template approval, policy update, business hours change, or assigning thread to another user; or machine key attempting assign/read.
403synthetic_tenantSend attempted from a synthetic demo tenant, which originates no outbound traffic. No payment changes this; use a real tenant.
404not_foundResource missing or belongs to another tenant.
409conflictAttempting to edit or approve a non-draft template version, or retiring an already retired version.
422sms_templates_onlyFree-text send or auto-reply attempted when tenant policy requires approved templates.
422from_requiredSend attempted without from line when provider has no default_from_e164.
422sms_from_invalidThe sender (from, or the provider's default_from_e164) is not an E.164 number.
422sms_from_not_ownedThe sender is E.164 but not an ACTIVE number of the tenant.
422did_unverifiedThe sender is an active number of the tenant, but did_ownership_proof is enforce and its ownership is not verified.
422no_rateNo sell rate covers the destination region; refused, and topping up does not change it.
422invalid_assigneeThread assignment attempted to a disabled, non-existent, or cross-tenant user.
422compliance_blockedSend blocked by compliance engine (Consent, DNC, QuietHours, SmsFrequency).
429throttled / sms_spend_cap_exceededRate limit or tenant monthly SMS spend cap reached.

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