# Compliance checks

Source: https://joinsimplesms.com/docs/compliance-checks
Index: https://joinsimplesms.com/llms.txt

Every message you send, through the API, the console, a broadcast, a
scheduled send, a recurring schedule or an automation, goes through the same
checks in the same order before it reaches a carrier. You call
`POST /v1/messages`; the checks are on by default.

A send that fails a check is **blocked**: it is not sent, it is not charged,
the API says why, and the attempt is in your log with the reason.

```
+14155550123   Blocked   Quiet hours
+12125550148   Blocked   Previously opted out
+13125550190   Delivered
```

## The checks, in order

The first check that fails is the one you hear about.

| # | Check | What it looks at | Returns | `failure.code` | Runs in |
| --- | --- | --- | --- | --- | --- |
| 1 | **Opt-out list** | The recipient has opted out of your messages (replied STOP, or was imported or recorded as opted out). One list for the whole account, every number and customer. | `403 recipient_opted_out` | `opted_out` | Live and sandbox. Send to +15005550011, or opt a number out with the consent API. |
| 2 | **Destination** | The number is in a US or Canadian area code. Other +1 regions are refused. | `403 forbidden` | `destination_not_allowed` | Live |
| 3 | **Sender registration** | A US local number may text the public only once it is linked to an approved registration. Until then it reaches only your verified numbers. | `403 sender_not_registered` | `sender_not_registered` | Live |
| 4 | **Per-recipient rate** | No more than 30 messages to one number in an hour. | `429 rate_limited` | `rate_limited` | Live. Send to +15005550012. |
| 5 | **Verified recipients (free accounts)** | A free account texts only numbers it has verified. | `403 recipient_not_verified` | `recipient_not_verified` | Live |
| 6 | **Quiet hours** | A marketing message outside 8:00am to 9:00pm in the recipient's local time (read from the area code) is refused, or held until 8:00am when you ask for that. Transactional messages and one-time codes are not checked. | `403 quiet_hours` | `quiet_hours` | Live and sandbox. Send to +15005550015 with category "marketing". |
| 7 | **Content screen** | Prohibited content and public link shorteners are refused; restricted categories are refused until approved for your account. | `403 forbidden` | `content_blocked` | Live |
| 8 | **Message allowance** | Your plan's daily and monthly message allowance. | `429 quota_exceeded` | `quota_exceeded` | Live and sandbox |
| 9 | **Spend limit** | The monthly spending limit you set, if you set one. | `403 spend_limit_reached` | `spend_limit_reached` | Live |
| 10 | **Prepaid credits** | The balance covers this message's price. | `402 insufficient_credits` | `insufficient_credits` | Live |

After the last check the message goes to the carrier. What a carrier does
with it afterwards (filtering, an unreachable phone) is a
[delivery failure](/docs/errors#delivery-failures), not a block.

This table is generated from the list the send pipeline is tested against,
so it cannot fall out of step with the code.

## What a blocked send looks like

The request answers with the status and code in the table, and a `failure`
object you can show to a person:

```json
{
  "error": {
    "code": "quiet_hours",
    "message": "It is 10:41pm for this recipient (America/New_York, from the number's area code). Marketing messages are sent between 8:00am and 9:00pm in the recipient's local time, so this one was not sent and nothing was charged. ...",
    "param": "to",
    "recipient_local_time": "22:41",
    "timezone": "America/New_York",
    "send_after": "2026-10-06T12:00:00.000Z",
    "category": "marketing",
    "category_basis": "request",
    "message_id": "msg_8Fq2ZkT0aLw4nXcV",
    "failure": { "code": "quiet_hours", "title": "Held back for quiet hours", "explanation": "...", "action": "Recommended: ...", "carrier_code": null },
    "request_id": "req_8Fq2ZkT0aLw4nXcV7pHs"
  }
}
```

The attempt is stored as a message with `status: "blocked"` and the same
`failure`. Its `price` is 0, it counts toward no allowance or limit, and
it is not part of the conversation with that number.

- **Console.** Logs shows the status **Blocked** and the reason. Filter by
  Blocked, then by reason. Compliance shows blocked sends for the last 7 and
  30 days, by reason.
- **API.** `GET /v1/messages?status=blocked` lists them;
  `&blocked_reason=quiet_hours` narrows to one reason. `failure_code`
  matches failed and blocked messages alike.
- **Webhook.** `message.blocked`, with `reason`, `message_id` and
  `failure`.
- **Export.** The messages CSV has `reason_code` and `reason` columns.

Two refusals a looping client can repeat without end (the per-recipient rate
and the message allowance) stop adding rows after 500 an hour per account.
They are still refused and still counted.

### Reason labels

The label is what Logs and the CSV show for each code.

| `failure.code` | Label | Status |
| --- | --- | --- |
| `carrier_filtered` | Filtered as spam | `failed` |
| `unreachable` | Phone unreachable | `failed` |
| `invalid_number` | Number not in service | `failed` |
| `opted_out` | Previously opted out | `blocked` |
| `content_blocked` | Content policy | `blocked` |
| `rate_limited` | Sending too fast | `blocked` |
| `landline` | Number can't receive texts | `failed` |
| `carrier_rejected` | Rejected by carrier | `failed` |
| `spend_limit_reached` | Spend limit | `blocked` |
| `insufficient_credits` | Out of credits | `blocked` |
| `sender_not_registered` | Sender not registered | `blocked` |
| `quiet_hours` | Quiet hours | `blocked` |
| `recipient_not_verified` | Not a verified test recipient | `blocked` |
| `quota_exceeded` | Quota | `blocked` |
| `destination_not_allowed` | Destination not served | `blocked` |
| `topic_unsubscribed` | Unsubscribed from topic | `blocked` |
| `unknown` | Not delivered | `failed` |

## Quiet hours

Marketing messages are sent between **8:00am and 9:00pm in the recipient's
local time**. That is the federal calling-time window for telephone
solicitations. A marketing message outside it is refused, or held until
8:00am, depending on how it was sent.

### Which messages are marketing

Only marketing messages are checked. In order:

1. **You said so.** `"category": "marketing"`, `"transactional"` or
   `"otp"` on the request always wins.
2. **A broadcast or batch** is marketing unless it says otherwise.
3. **A message from a number registered for the Marketing use case** is
   marketing.
4. **Everything else is transactional** and is sent at any hour.

So if you send codes, receipts and alerts today and do nothing, nothing
changes. A number registered as Mixed or Low-volume sends both kinds: label
its marketing sends with `"category": "marketing"`. The category is your
statement about your own traffic. We do not read message text to decide it.

Never checked, whatever the category: verification codes
([Verify](/docs/verify)), opt-out confirmations and HELP replies, a reply to
someone who texted that number in the last 4 hours, and sends to your
own verified test numbers.

### Local time

We read the recipient's time zone from the area code of their number, for
every US and Canadian area code. An area code tells us where a number was
issued, not where the phone is: someone who moved and kept their number is
read in their old time zone. It is the best signal available without
location data, and it is an approximation.

- An area code that spans two time zones is treated as quiet when it is
  quiet in **either** one.
- A number we cannot place (toll-free, or outside the US and Canada) is not
  held. Nothing is guessed.
- Daylight time, and the places that do not observe it (Arizona, Hawaii,
  Saskatchewan), follow the standard time zone database.

### Refuse or hold

```bash
curl https://api.joinsimplesms.com/v1/messages \
  -H "Authorization: Bearer $SIMPLESMS_API_KEY" -H "Content-Type: application/json" \
  -d '{"to": "+12125550148", "body": "20% off this week. Reply STOP to opt out.",
       "category": "marketing", "quiet_hours": "defer"}'
```

| `quiet_hours` | In quiet hours |
| --- | --- |
| `"block"` | Refused with `403 quiet_hours`. The body has `recipient_local_time`, `timezone` and `send_after`, the next allowed time. |
| `"defer"` | Held. The response is `202` with a [scheduled message](/docs/scheduled) set for the recipient's next 8:00am, plus a `deferred` object. It is listed and can be canceled like any scheduled message, and runs every check again when it is sent. |

Without `quiet_hours`, the default depends on what is sending:

| Sent by | Default |
| --- | --- |
| `POST /v1/messages`, the console composer | Refuse |
| `POST /v1/messages` with `scheduled_at` | Refuse when you schedule it, if the scheduled time is in quiet hours |
| Broadcasts and batches | Hold, recipient by recipient |
| Recurring schedules | Hold |
| Automations | Hold |

A broadcast sent at 10:00pm Eastern reaches recipients on the West Coast
right away and recipients on the East Coast at 8:00am their time.

A held message that is somehow still in quiet hours when it comes due is
held again, at most 3 times, and then blocked.

### Account default

Console → Compliance → Quiet hours sets the default for your account:

- **Refuse single sends, hold the rest** (the default).
- **Hold everything until 8:00am**, single sends included.
- **Off**, if you have your own quiet-hours handling. This is never a
  default, it takes a second confirmation, and it is recorded in your
  [audit log](/docs/audit-logs) as `quiet_hours.updated`.

A `quiet_hours` value on a request always wins over the account default.

### Test it

Quiet hours work the same with a test key. Two ways to trigger them:

- Send to `+15005550015` with `"category": "marketing"`. For this number
  it is always night.
- Send to any real-looking number with `"category": "marketing"`: it is
  read against its area code, as in live mode. Nothing is delivered.

### What quiet hours do not do

- They do not apply any **state** rule. Some states set narrower hours,
  different weekend hours, or holiday restrictions for solicitations. Only
  the federal 8:00am to 9:00pm window is applied. If you send into such a
  state, schedule accordingly.
- They do not know where a phone is, only where its number was issued.
- They do not decide whether a message is marketing. You do.

## What is not checked

These are not part of the pipeline. Nothing on this page should be read as
covering them.

- **Reassigned numbers.** We do not check whether a number has changed hands since the person gave you consent. No reassigned-number database is queried.
- **Litigator and complainer lists.** We do not screen recipients against any list of known litigants or serial complainants.
- **Do-not-call registries.** We do not check national or state do-not-call lists.
- **Consent itself.** We store the opt-in evidence you give us and enforce opt-outs, but we cannot verify that a recipient agreed to hear from you. That stays your responsibility.
- **State-specific sending hours.** Quiet hours use the federal 8:00am to 9:00pm window everywhere. Stricter state windows, weekend hours and holiday rules are not applied.
- **The recipient's actual location.** Local time is read from the number's area code, which says where the number was issued, not where the phone is.

You remain responsible for having consent for every recipient, for
classifying your traffic correctly, and for following the laws that apply to
your messages. These checks enforce specific rules by default; they are not
legal advice and do not make a messaging program compliant on their own. See
the [Messaging Policy](/messaging-policy).
