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
STOPinstantly suppresses the recipient and shows them in Settings → SMS log → Suppressions. - The SMS log shows status progression
queued → sent → delivered(orreadfor iMessage).
Setup — credentials
- Go to Settings → SMS → Providers.
- 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.
- Twilio. Paste your Account SID, Auth Token, and Messaging Service SID (recommended) or a single from-number.
- Dialpad (optional). Paste your API key. Used for voice features only; SMS does not flow through Dialpad in this pipeline.
- Click Save. We do a live credentials check against each provider; the row turns green when their API responds.
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:
- Open Clients → client.
- Click Send SMS in the action bar.
- 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).
- Pick the sender number if you have more than one (defaults to the store’s transactional number).
- Click Send. The message lands in the conversation timeline immediately with status
queued; the timeline updates in place as the provider ackssent → 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 tocore.sms_suppressionsimmediately. - Future sends to that number are
suppressedat queue time and never dispatched. - The reverse keywords
START,UNSTOP,YESremove 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). HELPauto-replies with the configured help text from Settings → SMS → Compliance.
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 → Compliance —
HELPreply, 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 —
staffand above;read_onlycan 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
| Symptom | Likely cause | Fix |
|---|---|---|
Test send shows queued forever | Provider credentials invalid | Re-paste keys at Settings → SMS → Providers; row goes green when valid. |
Sends to one number always failed | Recipient opted out | Confirmed in Settings → SMS log → Suppressions. The recipient must text START to opt back in. |
| Marketing campaign filtered to spam by carrier | 10DLC not registered for that brand | Register in Twilio / SendBlue console; meanwhile traffic is heavily throttled. |
| Inbound replies don’t thread to a client | Client’s phone number isn’t E.164-normalized | Open the unmatched message and drag onto the client; future replies thread. |
iMessage delivery never shows read | Recipient’s send-read-receipts is off | Behavioral; not a bug. delivered still fires. |
| Quiet hours fired on an MFA send | This shouldn’t happen | Capture the SMS log id and email {{CONTACT_EMAIL}}. |
The full symptom-to-fix table lives at Troubleshooting → SMS not delivered.
Related
- Email pipeline — the other half of outbound communications.
- Clients → Conversations — the inbox where inbound replies land.
- Auth & MFA — SMS is the optional second factor.
- Audit log — every send + every opt-out is recorded.
- Troubleshooting → SMS not delivered