# Errors

Source: https://jointextkit.com/docs/errors
Index: https://jointextkit.com/llms.txt

Every error is JSON with a stable envelope:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "`to` must be a valid US/Canada number in E.164 format.",
    "param": "to",
    "request_id": "req_8Fq2ZkT0aLw4nXcV7pHs"
  }
}
```

## Request IDs

Every response from an endpoint that takes your API key, success or error,
carries an `X-Request-Id` header
(`req_…`), and error bodies repeat it as `request_id`. Look it up in the
console under **Logs** to see the request and response as we received and
sent them (API keys, secrets, verification codes, and card data are
redacted; bodies are truncated at 8 KB; logs are kept for 30 days). Include
it when you contact support.

## Codes

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_request` | A parameter is missing or malformed; `param` names it. |
| 400 | `link_domain_required` | `shorten_links` was set but the account has no active link domain. Nothing was sent. See [link shortening](/docs/messages#link-shortening). |
| 401 | `invalid_api_key` | Missing, malformed, revoked, or unknown key. |
| 403 | `key_paused` | This live key is paused: it suddenly made far more requests than usual from a new country. An admin resumes or rolls it under **API keys**. See [unusual key activity](/docs/security#unusual-api-key-activity). |
| 403 | `number_purchase_held` | New live numbers are on hold: a number carriers were filtering was released and replaced within 7 days. Existing numbers keep working; contact support. See [replacing a filtered number](/docs/numbers#replacing-a-filtered-number). |
| 403 | `live_access_required` | Needs live access (or a live key before approval), or a live key for data the whole account shares once it is live ([test and live data](/docs/sandbox#test-and-live-data)). |
| 403 | `test_mode_only` | Sandbox-only endpoint called with a live key. |
| 403 | `tenant_suspended` | Account suspended. |
| 403 | `account_closed` | The account is closed, so nothing is sent or changed (test and live keys alike; its keys are revoked as well). See [closing your account](/docs/teams#closing-your-account). |
| 403 | `email_unconfirmed` | The account's email isn't confirmed yet: no new keys, webhooks or integrations until it is. Enter the emailed code in the console. |
| 403 | `forbidden` | Key is valid but can't act on this resource (e.g. a `from` you don't own). |
| 403 | `insufficient_scope` | A restricted key called outside its scopes; `required_scope` names the one needed. See [scopes](/docs/authentication). |
| 402 | `insufficient_credits` | Your [credit balance](/docs/credits) cannot cover the call; nothing was done or charged. The body carries `balance_micro_usd`, `required_micro_usd` and `top_up_url`. |
| 403 | `spend_limit_reached` | Your monthly [spend limit](/docs/spend-limits) is reached; the message was not sent or charged. |
| 403 | `recipient_opted_out` | The recipient opted out of your messages. The body says when and how. See [Opt-out](/docs/opt-out). |
| 403 | `quiet_hours` | A single marketing send (or any send with `"quiet_hours": "block"`) outside 8:00am to 9:00pm in the recipient's local time (8:00pm in Florida, Maryland and Oklahoma). Other messages are held, not refused. The body carries `recipient_local_time`, `timezone` and `send_after`; nothing was sent or charged. See [quiet hours](/docs/compliance-checks#quiet-hours). |
| 403 | `consent_required` | No opt-in on file for this message: a marketing message, or a recurring message from a number linked to a registration (which needs an opt-in for that registration); nothing was sent or charged. Record the opt-in (consent API with `campaign_id`, the console's Record opt-in, contact import, or a START reply), or, for a non-marketing message, send `consent` with it. See [opt-in on file](/docs/compliance-checks#opt-in-on-file). |
| 403 | `campaign_mismatch` | The message is outside the sending number's registration: promotional wording or `"category": "marketing"` on a non-marketing registration, anything but a one-time code from a two-factor registration, or a kind of message our registration check found doesn't match (`mismatch: "off_use_case"`, with `checked_by: "consistency_check"` and `fingerprint`). Nothing was sent or charged. The body carries `registration_id`, `use_case` and `mismatch`. See [which messages are marketing](/docs/compliance-checks#which-messages-are-marketing). |
| 403 | `reconfirmation_pending` | A marketing message to someone your registered number has not texted in 30 days: we asked them to confirm first and are holding this message (`held: true`), or the question is still open (`held: false`). See [re-confirmation](/docs/compliance-checks#re-confirmation-after-30-days). |
| 403 | `reconfirmation_expired` | They did not answer the re-confirmation within 7 days; marketing to them is refused until they opt in again. |
| 403 | `held_for_review` | A new kind of message from an account in its first 30 days of live sending: held for our team to review (`held: true`, with `fingerprint`, `registration_id` and `expires_at`), or not held because 10,000 of the same kind already wait (`held: false`). Nothing was charged. See [review of new kinds of message](/docs/compliance-checks#review-of-new-kinds-of-message). |
| 403 | `review_rejected` | Our team reviewed this kind of message for the sending number's registration and did not approve it; the reason is in the message. Nothing was sent or charged. |
| 403 | `campaign_paused` | Marketing from this number is paused because more than 4% of its registration's recipients opted out in 24 hours; nothing was sent or charged. Transactional messages still go out. See [opt-out rate](/docs/compliance-checks#opt-out-rate). |
| 403 | `account_paused` | All live sending from your account is paused while our team reviews messages that were blocked as phishing, impersonation or fraud (three to two or more people within 7 days). Your keys and data still work; batches and scheduled messages wait. Nothing was sent or charged. Reply to our email to appeal. |
| 403 | `sender_rotation` | This person was texted from two of your other numbers in the last 24 hours, so it was not sent from a third (carriers treat it as filter evasion). Text them from one of those numbers; a reply to someone who texted this number is never refused. Nothing was sent or charged. |
| 403 | `recipient_not_verified` | A free account sent to a number it has not verified. |
| 403 | `sender_not_registered` | The `from` number is not linked to an approved registration yet, so it can only text your verified numbers. `X-TextKit-Sender-State` says where it stands. See [Numbers](/docs/numbers#sender-status). |
| 404 | `not_found` | No such resource on your account, or it belongs to the other mode: a test key sees only sandbox data, a live key only live data ([test and live data](/docs/sandbox#test-and-live-data)). |
| 405 | `method_not_allowed` | The path exists but not for this HTTP method. The `Allow` header lists the ones it supports |
| 409 | `idempotency_conflict` | Idempotency-Key reused with a different payload. |
| 409 | `idempotency_in_progress` | The first request with this key is still running; retry after `Retry-After` (the SDKs do). |
| 409 | `verification_already_approved` | The verification was already approved; send a new code to verify again. |
| 429 | `rate_limited` | Too many requests, or the sending number's registration is over its [sending pace](/docs/compliance-checks#sending-pace); check `Retry-After`. |
| 429 | `quota_exceeded` | A daily quota was reached. |
| 503 | `screen_unavailable` | Our content screen could not check the message just now, so it was not sent or charged (we never send a message we could not check). Send it again after `Retry-After`; batches and scheduled messages wait and retry for about 15 minutes before giving up. See [content screen](/docs/compliance-checks). |
| 503 | `channel_unavailable` | [Verify](/docs/verify#by-email) email codes cannot go out right now. Nothing was sent or charged; send a text instead. |
| 502 | `carrier_error` | The carrier permanently rejected the message, or did not answer after we handed it over (so we will not risk sending it twice). Temporary carrier problems do not return this: the send answers `202` with the message `queued` and we retry it ([Messages](/docs/messages#automatic-retries)). |
| 500 | `internal_error` | Our fault. Retry, and tell us if it persists. |

## Delivery failures

A message that was accepted but not delivered has `status: "failed"` and a
`failure` object, on the message and in the `message.failed` webhook:
`{ code, title, explanation, action, carrier_code }`. The codes are stable;
switch on `code`, show `title` and `explanation` to your team, and follow
`action`. `carrier_code` is the carrier's own code when it sent one, for
support conversations.

| Code | Title | What happened, and what to do |
| --- | --- | --- |
| `carrier_filtered` | Filtered as spam | Their carrier flagged it as spam, so it never reached the phone. **Recommended:** say who you are up front, drop link shorteners and all-caps, and change wording that keeps getting filtered. |
| `unreachable` | Phone unreachable | The phone was off, out of coverage or full, and the carrier gave up. **Recommended:** retry in a few hours, and confirm the number if it keeps failing. |
| `invalid_number` | Number not in service | That number doesn't exist or isn't in service. **Recommended:** stop texting it and ask for a current number. |
| `opted_out` | Recipient opted out | They opted out of your texts, so we didn't send it. **Recommended:** don't retry. They can opt back in by texting START. |
| `campaign_mismatch` | Outside the registration's use case | This number's registration doesn't cover this kind of message, so we didn't send it. **Recommended:** send it from a number registered for it, or cut the promotional wording. |
| `content_blocked` | Content blocked | Carriers don't allow this content, usually a banned topic or a blocked link. **Recommended:** change the wording or link (see the Messaging Policy) and send again. |
| `screen_unavailable` | Content check unavailable | Our content check was briefly down, so we didn't send it. That's on us. **Recommended:** send it again after Retry-After. Batches and scheduled sends retry on their own for 15 minutes. |
| `rate_limited` | Sending too fast | Too many texts went out too fast for this number, registration or carrier, so we held this one. **Recommended:** retry with backoff and honor Retry-After. If a carrier's daily cap was hit, try tomorrow. |
| `landline` | Number can't receive texts | That's a landline or another line that can't get texts. **Recommended:** get a mobile number. GET /v1/lookup shows the line type before you send. |
| `carrier_rejected` | Rejected by the carrier | The carrier refused it, usually because this sender isn't allowed to reach this number. **Recommended:** check the sending number is set up for this kind of message. If one carrier rejects everything, send us the message id. |
| `spend_limit_reached` | Spending limit reached | It would have gone over your monthly spend limit, so we didn't send it. **Recommended:** raise the limit in Settings (or PATCH /v1/spend-limit) and send it again. |
| `insufficient_credits` | Out of credits | You're out of credit, so we didn't send it. **Recommended:** add credits in Billing, or turn on auto-recharge, and send it again. |
| `sender_not_registered` | Sending number not registered yet | Carriers haven't approved this number yet, so it can only text numbers you've verified. **Recommended:** open the number in Numbers to see where approval stands, and send again once it's Active. |
| `quiet_hours` | Held back for quiet hours | It was outside 8am to 9pm (8pm in Florida, Maryland and Oklahoma) in the recipient's time zone, so we didn't send it. **Recommended:** send it after send_after, or pass quiet_hours: "defer" and we'll send it when the window opens. |
| `consent_required` | No opt-in on file | Marketing and recurring texts need an opt-in on file, and this recipient has none. **Recommended:** record it first with POST /v1/consent/{phone}. For non-marketing texts, pass "consent": {"source", "collected_at"} instead. |
| `reconfirmation_pending` | Waiting for the recipient to confirm | You hadn't texted them in 30 days, so we asked them to confirm they still want marketing. **Recommended:** nothing. It sends if they reply YES within 7 days. |
| `reconfirmation_expired` | Recipient did not confirm | They didn't confirm they still want marketing within 7 days, so we dropped it. **Recommended:** don't retry. Marketing resumes if they reply YES or START, or you record a new opt-in. |
| `held_for_review` | Waiting for review | In your first 30 days live we review each new kind of message, and this one is waiting on us. **Recommended:** nothing. We usually review within a business day, then it sends. |
| `review_rejected` | Not approved in review | We didn't approve this kind of message for this registration. The reason is in your audit log. **Recommended:** match what you send to the registration, or email support if we got it wrong. |
| `review_expired` | Review did not finish in time | We didn't review it within 3 days, so we dropped it. That's on us. **Recommended:** send it again. |
| `campaign_paused` | Paused for this registration | Sending from this registration is paused, either for over 4% opt-outs in 24 hours or for our review. **Recommended:** make sure everyone you text opted in, then email support to resume. |
| `sender_rotation` | Too many of your numbers | Two of your other numbers already texted this person in the last 24 hours, and carriers read a third as dodging filters. **Recommended:** text each person from one number. A pool with sticky sender does this for you. |
| `recipient_not_verified` | Not a verified test recipient | Free accounts can only text verified numbers, and this isn't one. **Recommended:** verify it under Home, Test recipients, or add credits in Billing to text anyone. |
| `quota_exceeded` | Message allowance reached | Your daily or monthly message allowance ran out, so we didn't send it. **Recommended:** wait for the reset (midnight UTC daily, the 1st monthly) or add credits, then send it again. |
| `destination_not_allowed` | Destination not served | We only text US and Canadian numbers. **Recommended:** remove the number, or tell us which country you need. |
| `account_closed` | Account closed | This account is closed, so it can't send. **Recommended:** if we closed it by mistake, email support before the data is deleted. |
| `account_suspended` | Account suspended | This account is suspended, so it can't send. **Recommended:** email support. We'll tell you why and how to restore it. |
| `account_paused` | Sending paused for review | Live sending is paused while we review messages blocked as phishing, impersonation or fraud. **Recommended:** reply to our email to appeal, saying who sent them and why. |
| `topic_unsubscribed` | Unsubscribed from this topic | They unsubscribed from this topic, so we didn't send it. **Recommended:** don't retry. They still get your other topics. |
| `unknown` | Not delivered | The carrier says it wasn't delivered and won't say why. **Recommended:** retry once later. If it keeps failing, send us the message id. |

Some sends are refused by one of our [checks](/docs/compliance-checks)
before any carrier sees them: the recipient opted out, it is quiet hours for
a marketing message, the sending number is not registered yet, the content
is blocked, the number is rate limited, or the send would pass your
[spending limit](/docs/spend-limits) or your credit balance. Those answer
`402`, `403` or `429`, the error carries the same `failure`
object, and the attempt is stored as a message with `status: "blocked"`
(its id is `message_id` in the error), so one handler covers both and
your log shows every attempt:

```json
{
  "error": {
    "code": "recipient_opted_out",
    "message": "This recipient opted out of your messages on Oct 5, 2026 at 8:42 AM UTC by replying STOP. Nothing was sent and you were not charged. They can opt back in by texting START to your number.",
    "param": "to",
    "opted_out_at": "2026-10-05T08:42:00.000Z",
    "opted_out_via": "sms_keyword",
    "opted_out_method": "keyword",
    "failure": { "code": "opted_out", "title": "Recipient opted out", "explanation": "...", "action": "Recommended: ...", "carrier_code": null }
  }
}
```

Every code except `content_blocked`, `screen_unavailable`, `spend_limit_reached`,
`insufficient_credits` and `sender_not_registered` (sandbox numbers are never registered) has a
[sandbox number](/docs/sandbox) that produces it, so you can test the handling
before going live.

## Success codes

`200` and `201` mean done. `202` (on `POST /v1/messages`) means
accepted and `queued`: a temporary carrier problem is being retried for you.
A replayed idempotent request returns the original status code with an
`Idempotent-Replayed: true` header.

## Rate limits

Default 60 requests/minute per key. `429`s include a `Retry-After` header.
Successful quota-limited calls include `X-Quota-Remaining`.
