Quick check

Run through these four before going deeper.

  • The send shows up in Settings → SMS log.
  • That row’s status is delivered — not queued, sent, failed, or opted_out.
  • The recipient’s number is in E.164 format (+15551234567) on their client record.
  • The recipient hasn’t replied STOP previously (look for an “Opted out” chip on the client detail page).

Recipient opted out

The symptom

SMS log row shows status = opted_out. No message was actually sent. The client detail page shows an Opted out chip next to the phone number.

Why it happens

The recipient replied STOP (or one of the carrier-recognized variants: UNSUBSCRIBE, CANCEL, END, QUIT) at some point in the past. Carrier regulations are strict about this — once a number opts out, we’re legally required to stop sending until they explicitly opt back in by replying START or UNSTOP.

The opt-out is global per number, across every template, every campaign, and every store. It is not per-conversation.

How to fix it

  1. Confirm the opt-out is real — click the chip on the client detail to see the timestamp and the inbound message that triggered it.
  2. If the recipient asks you in person to resume texts, instruct them to reply START from their phone. We can’t flip the opt-out flag from the admin side — the recipient has to do it.
  3. Meanwhile, use email or a phone call instead. The client’s contact preferences card shows what other channels are available.
Don’t override opt-outs

The carrier compliance gate on core.fn_sms_check_opt_out is intentionally not overridable from the operator UI. Forcing a send to an opted-out number creates regulatory exposure and gets your sender number flagged for revocation.

Invalid phone number format

The symptom

Log row shows status = failed, error_code = invalid_phone (or Twilio code 21211).

Why it happens

The number on the client record isn’t in E.164 format. We require:

  • Leading +
  • Country code (e.g. 1 for US/Canada)
  • National number with no spaces, dashes, parens, or dots

So +15551234567 — not (555) 123-4567 or 555-1234567 or 1 555 123 4567.

How to fix it

  1. Open the client detail page. Edit the phone field.
  2. Reformat to E.164: prepend + and the country code, strip all separators. For US numbers that’s +1 + the 10-digit number, e.g. +15551234567.
  3. Save. The recipient’s normalized number is what subsequent sends use.
  4. Retry the action that triggers the SMS. Next log row should be delivered.

Number is a landline

The symptom

Log row shows status = failed, error_code = 30006 (Twilio) or landline_unsupported (SendBlue).

Why it happens

The carrier reports the number is a landline or VoIP line that doesn’t accept SMS. There’s no workaround on our side — if the carrier won’t terminate the message, nothing we send will land.

How to fix it

  1. Confirm with the recipient that the number is a mobile (text-capable). If they have a different mobile number, update the client record to use that one.
  2. If the recipient genuinely only has a landline, set their contact preferences to email-only and use email instead.
  3. If you have many landline-classified numbers that you’re sure are actually mobile, write to {{CONTACT_EMAIL}} with samples — carrier line-type lookups occasionally lag for ported numbers.

Sender number not provisioned

The symptom

Log row shows status = failed, error_code = sender_not_provisioned or Twilio 21606 / 21659.

Why it happens

The from-number on the SMS template (or your org’s default sender) isn’t actually provisioned in the SendBlue or Twilio account that’s sending. Common causes:

  • You set up a new org with a sender number that was never actually bought through the provider.
  • The provider reclaimed the number for non-payment or A2P registration lapse.
  • The number is registered to one Twilio sub-account but you’re sending from a different one.

How to fix it

  1. Open Settings → SMS → Sender numbers. Confirm the default sender for the org has a green Active badge.
  2. If it’s not active, open the provider dashboard (SendBlue or Twilio). Confirm you actually own the number and that your A2P 10DLC registration is current.
  3. If the number was reclaimed, you’ll need to buy a new one and update the org default sender.
  4. Retry the SMS. Next log row should land at delivered.

Held because of quiet hours

The symptom

Log row shows status = scheduled, with a future send_at timestamp. The recipient gets it later — not at the moment you triggered the send.

Why it happens

Per-recipient or per-org quiet hours deliberately delay non-urgent SMS that would otherwise land overnight. For example, if quiet hours are 21:00–08:00 local time and you trigger a marketing SMS at 23:00, we schedule it to fire at 08:01 the next day.

This is by design — sending overnight tanks open rates and gets you reported.

How to fix it

  1. If this is a transactional SMS that needs to land immediately (OTP, urgent order status), mark the template as category = urgent in Settings → SMS templates. Urgent SMS bypasses quiet hours.
  2. If quiet hours are too restrictive for your business, edit them in Settings → SMS → Quiet hours. They’re org-wide (or per-recipient when the client explicitly set theirs).
  3. If the delay is fine, wait — the SMS will fire automatically when the window opens.

Monthly quota exceeded

The symptom

Log row shows status = failed, error_code = quota_exceeded. Settings → SMS → Usage shows you at or over your monthly cap.

Why it happens

Each org has a monthly SMS budget configured in core.sms_settings. Hitting it stops further sends until the cap resets at the start of the next month (or you raise the cap).

How to fix it

  1. Confirm the usage in Settings → SMS → Usage. It shows the count, the cap, and the reset date.
  2. If you need more headroom this month, see Billing — the SMS add-on is metered separately.
  3. Once you raise the cap (or the month rolls over), retry. The next log row should fire normally.
  4. Long-term, audit which templates fire most. A noisy “every status update” template can chew through quota fast — consider digesting into one daily summary.

Carrier-level block

The symptom

Log row shows status = failed, error_code = carrier_blocked or Twilio 30007. Same body to other numbers works fine.

Why it happens

The carrier (T-Mobile, Verizon, ATT) has filtered this message based on content or sender reputation. Common triggers: URL shorteners, certain keyword patterns (“FREE”, “winner”), or your sender number being flagged for unrelated traffic.

This is rare for normal transactional templates and unfortunately opaque — carriers don’t tell us exactly why.

How to fix it

  1. Re-word the template to drop URL shorteners (use the full https://app.peptideclients.com/… URL instead).
  2. Drop suspicious phrases. Test with a clearly-transactional version first.
  3. If the carrier block persists across templates, try switching providers for that org. Set the org’s sender to a different number on the alternate provider in Settings → SMS → Sender numbers.
  4. For sustained carrier issues, write to {{CONTACT_EMAIL}} with the failing message body and the recipient’s carrier (if known).

Reading the diagnostics

Settings → SMS log lists every send attempt in the last 90 days. Columns:

ColumnWhat to look at
sent_atWhen we enqueued the message. Compare with delivered_at for latency.
toRecipient number, normalized to E.164. Confirm the format is right.
bodyFirst 80 characters of the rendered message. Useful to confirm template variables filled in.
providersendblue (primary) or twilio (fallback). If you see traffic on the fallback, the primary is degraded — check provider status pages.
statusqueuedsentdelivered. Anything else (failed / opted_out / scheduled / suppressed) jumps to one of the sections above.
error_codeEither our string (invalid_phone, quota_exceeded) or the upstream provider code (Twilio 30006, etc.).
provider_message_idProvider’s id. Click to deep-link to SendBlue or Twilio for the full upstream event timeline.

When to write to support

Email {{CONTACT_EMAIL}} with:

  1. The recipient number (E.164) and the approximate send timestamp.
  2. The SMS log rowCopy as text. Includes status, error_code, provider_message_id.
  3. The sender number the org is configured to use.
  4. If you have it, a screenshot of the provider dashboard’s detail page for that message id.
  5. One sentence each on expected vs actual.

Related: SMS feature guide · Billing & quotas · Troubleshooting hub.