Overview

{{COMPANY_SHORT_NAME}} sends SMS through two providers and falls back automatically.

  • SendBlue — preferred for US/CA recipients. Detects iMessage capability and delivers as a blue-bubble message with read receipts when possible; falls through to SMS otherwise. Used for most operator-to-client messaging.
  • Twilio — the fallback for plain SMS, plus the carrier of record for short-code traffic and non-US numbers.
  • Dialpad — used for voice (calling, voicemail). Out of scope here but tied to the same per-store sender configuration.

Outbound goes through the send-sms edge function. Inbound webhooks land at sendblue-webhook, get dedupe-checked, and thread into the conversation timeline for the matched client. STOP / UNSUBSCRIBE keywords are honored automatically and add the recipient to the per-org SMS suppression list.

What working correctly looks like

  • Settings → SMS → Providers shows green credentials checks for SendBlue and Twilio.
  • Sending a test from Settings → SMS → Send test arrives at the test number within seconds.
  • Each store has at least one sender number assigned (transactional by default).
  • Inbound replies appear in the matched client’s timeline and in the workspace inbox within seconds.
  • Replying with STOP instantly suppresses the recipient and shows them in Settings → SMS log → Suppressions.
  • The SMS log shows status progression queued → sent → delivered (or read for iMessage).

Setup — credentials

  1. Go to Settings → SMS → Providers.
  2. SendBlue. Paste your API key and signing secret. The signing secret is what we HMAC-verify inbound webhook payloads with — pick one and paste the same value into SendBlue’s webhook config.
  3. Twilio. Paste your Account SID, Auth Token, and Messaging Service SID (recommended) or a single from-number.
  4. Dialpad (optional). Paste your API key. Used for voice features only; SMS does not flow through Dialpad in this pipeline.
  5. Click Save. We do a live credentials check against each provider; the row turns green when their API responds.
10DLC registration is on you

If you’re sending via Twilio in the US, your numbers and your brand must be 10DLC-registered with the Campaign Registry. Unregistered traffic is heavily filtered or blocked. SendBlue handles registration for the numbers it provisions; for Twilio numbers you bring, register them in your Twilio console before you send anything other than test traffic.

Sender numbers

Sender numbers are assigned per-store and tagged by purpose so transactional traffic doesn’t share a number with marketing.

  • Transactional — receipts, fulfillment updates, MFA codes, magic-link fallback. Required.
  • Marketing — campaigns, drip flows. Required to honor STOP / HELP keywords. Recommended to use a different number from transactional so a STOP on a campaign doesn’t suppress receipts.

Configure at Settings → SMS → Numbers while the target store is active. Each row pins:

  • The phone number (E.164).
  • The provider that owns it (SendBlue or Twilio).
  • The purpose (transactional / marketing).
  • Optional friendly name shown to operators (e.g. “Retail orders line”).

Sending a message

From a client’s detail page, the most common path:

  1. Open Clients → client.
  2. Click Send SMS in the action bar.
  3. Pick a template (or compose freely). The compose box shows live merge-field substitution and a character counter (160 for SMS, 70 for SMS with non-GSM characters; iMessage has no segment limit).
  4. Pick the sender number if you have more than one (defaults to the store’s transactional number).
  5. Click Send. The message lands in the conversation timeline immediately with status queued; the timeline updates in place as the provider acks sent → delivered → read.

Templates & merge fields

Templates live alongside email templates in core.email_templates (despite the name — the table holds both). SMS templates have a single body field, no subject, and the same Mustache-style merge fields.

Common merge fields:

  • {{client.first_name}}
  • {{order.number}} · {{order.total}} · {{order.tracking_url}}
  • {{store.name}} · {{operator.first_name}}
  • {{magic_link}} for sign-in fallback

Edit at Settings → SMS → Templates. Per-org overrides work the same way as in Email templates.

Two-way replies

Replies fan in through sendblue-webhook (and the equivalent Twilio inbound webhook).

  • The webhook matches the From number against the workspace’s clients (E.164 normalized).
  • If matched, the message threads into that client’s conversation and lands in the workspace inbox.
  • If unmatched, the message lands in the inbox under “Unmatched senders”; you can drag it onto a client to bind the number going forward.
  • Per-thread seen state is per-operator: opening the thread marks it read for you, not for the rest of the team.

The inbox itself is documented under Clients → Conversations.

Opt-out compliance

STOP-keyword handling is automatic and aggressive. The compliance bar is high here so your sender reputation stays clean.

  • Inbound message body matches (case-insensitively) one of STOP, STOPALL, UNSUBSCRIBE, CANCEL, END, QUIT, OPTOUT → recipient appended to core.sms_suppressions immediately.
  • Future sends to that number are suppressed at queue time and never dispatched.
  • The reverse keywords START, UNSTOP, YES remove the suppression but only if the same number sent it (the recipient must opt back in themselves; an operator can’t un-suppress on their behalf).
  • HELP auto-replies with the configured help text from Settings → SMS → Compliance.
Operators cannot un-suppress an SMS opt-out

This is on purpose. The CTIA / TCPA rules say only the recipient can revoke their opt-out. The unsuppress button is hidden in the UI; calls from the API also fail with 403 sms_optout_locked.

SMS log

Settings → SMS log mirrors the email log:

  • Status — queued, sent, delivered, read (iMessage), failed, suppressed.
  • Provider, sender number, recipient, body, segment count.
  • Timestamps — queued_at, sent_at, delivered_at, read_at, failed_at.
  • Provider message id (clickable to the provider console).
  • Cost in USD per send (carrier surcharge + provider fee).

The Suppressions tab lists every opt-out with the keyword that triggered it.

Quiet hours

Per-recipient quiet hours prevent transactional sends during sleep or local nighttime. Configured per-client at Clients → client → Communications → Quiet hours:

  • Window — e.g. 9pm–7am.
  • Time zone — defaults to the client’s zip-code-derived TZ; override manually if known.
  • Channels — SMS only by default; can also block voice.

During the quiet window:

  • Marketing sends are deferred to the next allowed hour.
  • Transactional sends fire normally (you don’t want a delivery confirmation held overnight).
  • MFA sends always fire (security beats etiquette).

An org-wide default applies when a client has no per-row override; configure at Settings → SMS → Quiet hours.

Settings & permissions

  • Settings → SMS → Providers — credentials for SendBlue, Twilio, Dialpad. Owner / admin.
  • Settings → SMS → Numbers — per-store sender numbers and purposes. Owner / admin.
  • Settings → SMS → Templates — per-org template overrides. Owner / admin.
  • Settings → SMS → ComplianceHELP reply, opt-out keywords (read-only), legal footer. Owner / admin.
  • Settings → SMS log — visible to all roles for messages in their granted stores; suppression view is admin-only.
  • Sending a message — staff and above; read_only can view conversations but not send.

API + automation

The send-sms edge function is internal and is the canonical entry point for any SMS — even ad-hoc admin-triggered ones go through admin-sms, which calls into the same pipeline so the suppression list, quiet hours, and audit log all apply.

Programmatic sends fire when platform events transition (e.g. order shipped dispatches the order_shipped SMS template if your org has it enabled). There is no public “send arbitrary SMS” endpoint — same reasoning as email.

Troubleshooting

SymptomLikely causeFix
Test send shows queued foreverProvider credentials invalidRe-paste keys at Settings → SMS → Providers; row goes green when valid.
Sends to one number always failedRecipient opted outConfirmed in Settings → SMS log → Suppressions. The recipient must text START to opt back in.
Marketing campaign filtered to spam by carrier10DLC not registered for that brandRegister in Twilio / SendBlue console; meanwhile traffic is heavily throttled.
Inbound replies don’t thread to a clientClient’s phone number isn’t E.164-normalizedOpen the unmatched message and drag onto the client; future replies thread.
iMessage delivery never shows readRecipient’s send-read-receipts is offBehavioral; not a bug. delivered still fires.
Quiet hours fired on an MFA sendThis shouldn’t happenCapture the SMS log id and email {{CONTACT_EMAIL}}.

The full symptom-to-fix table lives at Troubleshooting → SMS not delivered.