# Message delivery errors

Canonical: https://tyxter.com/docs/delivery-errors
Section: Messaging

How provider failures surface on a message, the stable error codes they map to, and how to troubleshoot WhatsApp deliverability (Meta 131026 and friends).

# Message delivery errors

> **Compatibility note (July 2026).** Meta errors `131026` and `131042` previously used the generic `provider_error` Tyxter code. They now map to `meta_message_undeliverable` and `meta_business_eligibility_payment_issue`. If your integration branched on `error_code === 'provider_error'`, add the specific codes while continuing to read the numeric Meta code from `provider_error.code`.

> **Compatibility note (August 2026).** Meta error `131049` previously used `provider_error`. It now maps to `meta_marketing_frequency_cap`, a recipient-scoped terminal result that must not pause the sending number. Continue reading the numeric Meta code from `provider_error.code` when handling older records.

> **Compatibility note (August 2026).** Meta error `131037` previously used `provider_error`. It now maps to `meta_display_name_not_approved`. Continue reading the numeric Meta code from `provider_error.code` when handling older records.

> **Compatibility note (August 2026).** Historical Meta `132001` rows may carry `template_param_mismatch`; new receipts carry `template_not_approved`. Continue reading the numeric Meta code from `provider_error.code` when handling older records.

When a WhatsApp send fails, the message record carries three fields you can read at GET /v1/messages/{message_id}: `error_code` — a stable, registered Tyxter code that is safe to branch on; `error_message` — a human-readable reason that, for a mapped code, ends with the next step to take; and `provider_error` — the raw error object from Meta (numeric `code`, `error_subcode`, title and details text, and Meta's `fbtrace_id`), preserved verbatim for diagnosis. Failures reach you two ways: rejected inline when you create the message, or reported later by Meta and delivered to your webhook endpoint as a `message.failed` event. Both paths record the same fields on the message, and the `message.failed` webhook payload carries `error_code`, `error_message` and `provider_error` inline — a receiver never has to call back to learn why the send failed.

`error_code` is bounded — it is never a raw provider string. Known Meta failure codes map to specific Tyxter codes, and everything else maps to `provider_error` with the Meta detail preserved:

- Meta `190` maps to `provider_connection_unauthorized`: the stored WhatsApp connection credential is expired or revoked. Every failed message includes an organization-targeted `/connect-whatsapp?organization_id=…` link to re-run Embedded Signup. Retrying without reconnecting cannot succeed.
- Meta `131047` maps to `service_window_required`: a free-form message was sent outside the 24-hour customer service window (opened by an inbound message from the recipient). Send an approved template instead — list them at GET /v1/templates.
- Meta `131008` for template messages, plus `132000` and `132007`, map to `template_param_mismatch`: this is the asynchronous fallback when Meta rejects an already accepted send because a required template parameter is missing, the template name, language, or parameters no longer match the approved version, or the provider behavior is not modeled. `131008` on a non-template message remains `provider_error` because Meta uses that code generically. Compare the send against the approved version before retrying. Modeled exact POSITIONAL/NAMED BODY keys, named text-HEADER and dynamic URL `parameter_name` values, media headers, and dynamic URL-button parameters instead fail synchronously with `400 template_param_mismatch` before a message exists, so they carry no `provider_error`.
- Meta `132001` maps to `template_not_approved`: Meta could not resolve the exact template name and language as approved in the sending WhatsApp Business Account. Check that exact name and language, then confirm approval before sending again. This terminal receipt does not identify why Meta could not resolve the template; the raw Meta code remains in `provider_error.code`.
- Meta `131042` maps to `meta_business_eligibility_payment_issue`: Meta blocked the send over the business's billing setup. Complete the business tax information and full address, then add or update the payment card in the billing section of Meta Business settings. Every send from the account fails until it clears, so this one is worth alerting on.
- A fresh stored WABA block terminalizes a queued send before Meta is called, with a stable `error_code` and `provider_error: null`. When a Meta send error causes reactive health confirmation to prove that the effective sender WABA is blocked, the same terminal receipt is not retried unchanged and retains the original bounded send failure in `provider_error`, even when its provider code was generic. Meta health code `141006` records `meta_payment_method_required`: add a payment method in WhatsApp Manager → Billing & payments. `141008` records `meta_waba_inactive`: reactivate the WABA in WhatsApp Manager or ask Meta support. `141011` records `meta_messaging_permission_missing`: reconnect and grant the requested WhatsApp messaging permissions, then ask support if it persists. If Meta reports more than one recognized diagnosis, remediation is deterministic: payment, inactive WABA, then messaging permission. This does not suspend the connection or phone number, and inbound messages remain available. After the account is fixed, a positive Meta health observation clears that WABA's capability evidence so a later send can continue. Read the connection's optional `waba_send_capabilities` entry matching the sender `waba_id` for the latest trusted diagnosis. The singular `send_capability`, `send_block_codes`, and `send_capability_observed_at` fields are the compatibility view for the connection's primary WABA.
- Meta `131037` maps to `meta_display_name_not_approved`: a WhatsApp-provided (+1 555) number needs a display name. In WhatsApp Manager, go to Phone numbers and set the display name, then wait for Meta to approve it before sending again. This terminal receipt does not pause the sending number.
- Meta `131053` maps to `meta_media_download_failed`: Meta could not fetch or could not use the media for the send — the host was unreachable or too slow, returned an error, or served a file whose real content did not match the type it claimed. When the send carried a link you control, that link is the thing to fix: make sure it is anonymously reachable and serves the correct type, because retrying the same link usually fails the same way. The most reliable fix is to stop sending links at all — upload the file at POST /v1/media/uploads and send it by asset_id. This code can also mean the bytes themselves are the problem, including for media Tyxter hosts: Tyxter validates the `mime_type` you declare, not the bytes you send, so a HEIC photo saved as `.jpg` and declared `image/jpeg` passes upload and still fails here. Two cases can no longer reach this code. Media uploaded through POST /v1/media/uploads or sent inline on POST /v1/messages has its declared `mime_type` checked against WhatsApp's accepted set up front and is rejected with `invalid_media_mime_type` instead of failing here. And a WhatsApp send carrying a `media.link` is no longer handed to Meta as a link at all — Tyxter retrieves the file itself and sends the uploaded copy, so a URL Tyxter cannot retrieve now reports `media_link_fetch_failed` and a URL serving a type WhatsApp does not accept now reports `invalid_media_mime_type`, both before the send is submitted. See "Media links Tyxter retrieves" below. What still reaches `131053` is media Meta receives but cannot use, and the narrow link cases Tyxter does not retrieve.
- Meta `131026` maps to `meta_message_undeliverable`: Meta could not deliver to that recipient. Retry addressing them by phone number rather than a business-scoped user id, and remove links from the message if you are sending to one. See the section below.
- Meta `131062` maps to `meta_business_scoped_id_mismatch`: a business-scoped user id issued under one business portfolio was used to send from another. These ids belong to a single portfolio — send using the id issued by the portfolio that owns this phone number.
- Meta `130472` maps to `meta_marketing_experiment_holdout`: the recipient is in Meta's marketing-message holdout group, so Meta withholds marketing sends to them. This is an expected loss rather than a fault to fix; utility, service, and authentication messages are not affected.
- Meta `131049` maps to `meta_marketing_frequency_cap`: Meta withheld this marketing message because the recipient recently received too many marketing messages. Do not retry it today. This is scoped to the recipient and does not pause the sending number; utility, service, and authentication messages to the same recipient are not affected.
- Anything else maps to `provider_error`: Meta accepted the request shape but reported a failure Tyxter has no specific mapping for. Interpret the numeric code inside the message's `provider_error` field, which is always preserved.

## Failures that never reach Meta

Seven codes are stamped before any provider call, so a message carrying one has no `provider_error` to read — nothing was sent and nothing was billed. They are as stable and as safe to branch on as the mapped codes above.

- `no_provider_connection`: the environment has no WhatsApp connection at all, so there is nothing to send through. Connect WhatsApp from the dashboard, then resend. Keep it apart from the three codes that mean a connection does exist: `provider_connection_unauthorized` (the stored credential died — re-authenticate), `provider_connection_suspended` (Meta acted against the account — appeal), and `meta_registration_required` (setup began and stopped before Meta finished registering the number — finish it from the dashboard link in the message text). Retrying without connecting cannot succeed.
- `sender_phone_number_unavailable`: an accepted message named a sending phone Tyxter cannot use. The receipt intentionally gives no more specific reason, does not fall back to the connection's primary number, and never reaches Meta. Choose an active WhatsApp phone linked to Meta, then resend. Older messages with no sender continue using their connection default for compatibility.
- `invalid_template_payload`: a template message reached the sender without both `template.name` and `template.language`, so there was no template to resolve. Resend with both fields set. It is not retryable unchanged.
- `phone_number_rate_limit_exhausted`: the send was rescheduled repeatedly because the sending phone number was over its throughput budget, and it was still over after the last reschedule. Nothing about the message is wrong — it lost a queue for capacity. Spread the traffic over a longer window, send from more numbers, or raise the phone number's throughput before resending.
- `media_asset_resolution_failed`: Tyxter could not resolve the media attached to the send and the failure carried no more specific cause. The expected media problems have their own codes and never land here — `media_asset_not_found`, `media_asset_expired`, and `media_asset_not_ready` for the asset itself, and `media_link_fetch_failed`, `invalid_media_mime_type` and `media_too_large` for a `media.link`. This is the residual bucket, so a repeat is worth reporting with the message's `trace_id` rather than retried indefinitely.
- `media_voice_requires_ogg_opus`: a requested WhatsApp voice note resolved to known audio whose normalized MIME is not `audio/ogg`, so delivery stops before a provider call. Meta requires OGG/Opus mono audio, but Tyxter checks only the MIME it knows; it does not inspect codecs or channels, or convert media. Use OGG/Opus mono audio or omit `message.media.voice` for ordinary audio.
- `provider_send_result_missing`: stale-send recovery found a message left in `sending` with no provider message id and zero Meta attempts after its immutable creation age reached the effective queue-expiry bound. Before that bound, the same provider-safe shape is returned to durable queued work with a retry delayed by 60 seconds, so it does not receive this error code. At or beyond the bound, Tyxter fails it instead of requeueing forever. A different shape — a Meta attempt started but no response or error was persisted — is never replayed automatically because Meta may already have accepted and billed the send; retrying that ambiguous outcome could deliver a duplicate. Report repeated `provider_send_result_missing` occurrences or a message that remains `sending` with its `trace_id`.

## TTS render failures

TTS audio is rendered before WhatsApp is called. `tts_provider_error` means the configured speech provider rejected or could not complete the render after retries; the message's `provider_error` carries a bounded `provider` name and, when available, `status_code`. `tts_render_failed` means rendering failed outside a provider response, such as storage or unexpected processing failure. In both cases the message never reached WhatsApp. Use the message `trace_id` when reporting a repeated `tts_render_failed`.

## Media links Tyxter retrieves

When a WhatsApp media message carries `message.media.link`, Tyxter now retrieves that file and uploads it to WhatsApp, then sends by the uploaded media id instead of passing your URL along. Nothing about the request changes: you still send `media.link`, GET /v1/messages/{message_id} still shows the message you created, and no new field appears. What changes is which failures you can see. Meta's own weblink fetch was the single largest source of `131053`, and it gave you nothing to act on except the word "failed"; a retrieval Tyxter performs can tell you exactly what went wrong, and it lets the accepted-type and size rules that already apply to uploads apply to links too. One more benefit is quiet but real: a broadcast retrieves and uploads the file once per sending phone number rather than once per recipient, so your origin is not hit thousands of times.

`media_link_fetch_failed` is the stable code for that retrieval failing. It means Tyxter could not get the bytes — the host did not resolve, refused the connection, or was rejected as a private or internal address; the request timed out; the URL answered a non-2xx status; it redirected more than three times; or it returned an empty body. Some of those causes are transient (DNS blips, timeouts, connection resets, `429` and `5xx` answers) and Tyxter retries them automatically, stamping the code only once the attempts are exhausted; the rest fail the message on the first attempt because retrying an unreachable or forbidden URL only repeats the same answer. To fix it, make the URL publicly reachable without authentication — https is strongly recommended and plain http is also accepted — serve the file bytes directly rather than an HTML preview or a login page, and answer within 30 seconds. The most reliable fix is to skip URLs entirely: upload the file once at POST /v1/media/uploads and send it by `asset_id`.

Two other codes can surface on the same retrieval, and both mean Tyxter got the bytes and refused them. `invalid_media_mime_type` means the URL answered without a Content-Type header at all, or with one WhatsApp does not accept for that media kind — the same accepted set POST /v1/media/uploads enforces, so a URL cannot deliver a type an upload of the same file would have been refused. `media_too_large` means the response exceeded the per-kind size limit; the download is cancelled mid-stream rather than read to the end.

Keep `media_link_fetch_failed` and `meta_media_download_failed` apart, because the remediation differs: the first means Tyxter could not retrieve your URL and the message never reached Meta, the second means Meta could not fetch or use media that was handed to it. For the same reason, `media_link_fetch_failed` never means a provider rejected your send — no provider was contacted.

Retrieval covers the `media` message type and the media header of an `interactive` reply-button message. A reply-button header carrying a `link` is retrieved and uploaded exactly like `message.media.link` — same accepted types, same size limits, and the same `media_link_fetch_failed` / `invalid_media_mime_type` / `media_too_large` codes when it fails; a header carrying `id` is a media id you already hold at Meta and is passed through as-is. Flow message headers — and the headers of interactive list messages — are text-only and never carry media at all. Template header media is always referenced by `asset_id` and is uploaded before the send, but a header image supplied as a raw link inside `template.components` bypasses this entirely and is passed through untouched. Still unretrieved: a link whose scheme is not http or https, channels whose provider has no media upload API (Instagram), and rare operational fallback modes. In sandbox, delivery is simulated and no URL is ever retrieved.

## When Meta has acted against your WhatsApp account

`provider_connection_suspended` is the stable code for a send that never left Tyxter because Meta acted against the WhatsApp Business Account behind the connection — it was banned, rejected at account review, restricted, legacy-suspended with `scheduled_for_disable`, or the grant was lost because Tyxter's app was removed or the account was deleted. It is not a deliverability problem with the recipient and it is not a Meta-mapped code: the connection is `suspended`, so every send on it fails before any provider call and there is no `provider_error` to read. That is deliberately wider than Meta's own action — a restriction or a legacy scheduled-disable suspension still leaves some sending possible at Meta, and Tyxter stops anyway rather than pushing traffic through an account Meta has already flagged. A connected row with current `SCHEDULE_FOR_DISABLE` evidence is not this error: its nullable `waba_ban_date` is advisory, and sends continue until Meta actually disables the account.

Retrying cannot succeed until the account state changes, so treat it as an operator alert rather than a retryable failure. Read `suspension_reason` on the connection at GET /v1/provider-connections/{connection_id} for the machine-readable reason, or the `whatsapp` channel's `reason` on GET /v1/provider-connections/status, which reports `policy_suspended` for this case and `token_suspended` when the credential merely died. The remedy follows the reason: appeal the account at business.facebook.com/accountquality when it was banned, rejected at review, restricted, or legacy-suspended for disablement — replacing or re-authorizing the access token cannot lift any of those, because the credential is not what Meta acted on — and reconnect WhatsApp from the dashboard when the reason is `app_uninstalled` or `account_deleted`, where connecting again really is the fix.

Keep it apart from `provider_connection_unauthorized`, which means the stored credential expired or was revoked and re-authenticating restores sends. Its write-path twin is `provider_connection_policy_suspended`, the `409` a credential write (rotate, register, complete registration) gets for the same account state. Sends resume on their own once Meta clears the account and the connection returns to `connected`.

## Meta 131026 — Message Undeliverable

The most common delivery failure on production sends is Meta code `131026` ("Message Undeliverable"), which maps to `meta_message_undeliverable`: Meta could not deliver to that specific recipient. It is recipient-specific and it is not transient — retrying the identical send will fail again. The strongest diagnostic is comparison: when the same template and sender deliver to other recipients, your template, sender, and Tyxter configuration are all working, and the cause is one of:

- The recipient cannot receive the message: the number is not an active WhatsApp account, runs a very old WhatsApp client, or the user has not accepted WhatsApp's current terms of service.
- Your WhatsApp Business Account has limited reach: an unverified business is capped to a small set of unique recipients contacted outside their customer-service windows. This is not a cap on the number of messages. Check the business verification status and messaging limits in Meta Business Manager / WhatsApp Manager.
- Meta declined this sender-to-recipient pair: the user blocked the sender, or Meta's per-user marketing-message limits throttled another marketing template to that user.
- You addressed a business-scoped user id and the message contains a link: the same `131026` comes back. Retry addressing the recipient by phone number, or send the message without the link.

To self-diagnose from Tyxter: read the sender's health at GET /v1/phone-numbers/{phone_number_id} — the response carries the Meta quality rating and messaging-tier metadata — and read the failing message's `provider_error` detail at GET /v1/messages/{message_id}. A `131026` failure originates inside Meta's delivery network; the send left Tyxter correctly, so there is no Tyxter-side configuration to fix.

In sandbox, delivery is simulated and no real phone is ever contacted, so Meta deliverability codes such as `131026` occur only on production sends. The sandbox scenarios instead stamp the registered codes `sandbox_delivery_failure`, `sandbox_recipient_opted_out`, and `sandbox_template_rejected`. These are deterministic terminal outcomes, carry no `provider_error`, and appear on the same message read and `message.failed` webhook surfaces as production delivery codes. See the Sandbox guide.
