SMS

Why are my SMS messages being filtered by carriers?

A filtered message does not announce itself. The send was accepted, the call returned, and the message went on its way. Filtering happens afterwards and somewhere else, at a carrier, once Bird has handed the message over. What comes back to you is a failure event with an error code attached, and which code you get is less obvious than the API reference makes it look.

How do I know a carrier filtered the message?

Read the event and the error code together. The event says what shape the failure took; the code says what caused it.

EventWhat happened
sms.undeliveredA non-permanent non-delivery: the handset was off or unreachable
sms.failedA permanent delivery failure stopped the message
sms.rejectedThe message was refused rather than attempted, by Bird's own checks, by a charge that could not complete, or by a carrier that turned it away
sms.expiredThe carrier stopped trying and reported the message expired

No single event means filtering. The same error code can ride three of them, because the event is chosen from the delivery receipt's status and the code from its reason: a rejection becomes sms.rejected, a permanent delivery failure becomes sms.failed, a recoverable one becomes sms.undelivered, and an expiry becomes sms.expired with the code still attached.

A rejection stops the message before a delivery attempt succeeds, and a message rejected during processing carries sms.rejected as its only event. Expiry always comes from the carrier's receipt: Bird sets no validity window of its own and runs no timer that ends a message.

Which failure codes will I actually see?

This is where the API reference misleads if you read it as a menu. It documents eleven standardized codes, and the pipeline emits a subset of them today.

What a delivery receipt can currently produce:

CodeArrives when
content_rejectedThe provider bucketed the failure as a carrier rejection. This is what carrier filtering normally looks like.
recipient_opted_outThe provider reported an opt-out
unreachableThe handset was off or out of coverage
invalid_destinationThe number is unknown to the network
provider_unavailableA network error, or a capacity limit
unknownThe reason did not map to any of the above

Two codes are set before a message is ever sent: insufficient_balance when the wallet cannot fund it, and unknown for a product with no price.

That leaves four codes the API defines and nothing emits yet: blocked_by_carrier, sender_unregistered, blocked_by_recipient and landline_unreachable. blocked_by_carrier is reachable only by sending to a magic test number. If you branch on it in production you will never take that branch, and if you search your event stream for it you will find nothing and conclude, wrongly, that no filtering is happening.

The practical consequence is worth stating plainly. A carrier filter reaches you as content_rejected, or as unknown when the provider's reason is not one Bird maps. The provider reports more distinct reasons than Bird normalizes, so unknown is a real bucket rather than an edge case.

The set is an open enum in both directions: match the codes you handle and accept an unrecognized value rather than treating the response as invalid.

Every failure also carries a description. On the delivery-receipt path this is the provider's own reason string, such as opted_out or carrier_rejected, falling back to its status text, rather than carrier prose you can show a customer. For failures Bird decides itself it is a real sentence, such as insufficient wallet balance. Because the field varies that much, branch on code and treat description as a hint.

What does recipient_opted_out really mean?

It means a carrier told us about an opt-out. It does not mean your own suppression list stopped the message, and the difference matters because the two happen at opposite ends of a send.

Your own suppression list is enforced when you call the API. A send to a suppressed pair is refused there and then, with E12077 and the message This recipient is suppressed for this sender and cannot be messaged. No message exists, so there is no event, no status and no error code to read.

recipient_opted_out is the other direction. The message left, something downstream reported that the recipient had opted out, and the code arrives on the failure event. Bird then records a suppression of its own from that report, because a carrier-reported opt-out is the carrier's record of a STOP we may never have seen. The suppression is stamped with the carrier's report time rather than the moment the job ran, so a later opt-in cannot be overtaken by a delayed report.

So the pair a reader meets in practice is: the first send fails with recipient_opted_out, and the next send to that pair is refused at the API.

What is in carrier_error_code, and why is it often empty?

This field is misnamed and we have kept the name deliberately. It does not carry the carrier's own code. It carries the sending platform's raw code, which is finer-grained than the code that normalizes it, and it is not a Bird-defined value. Quote it to support when you ask why a message failed.

It is null in two cases, and the second surprises people:

  • The provider sent no code.
  • No provider was reached at all. A failure Bird decided before handing the message over has no provider code by construction, so an insufficient_balance rejection never carries one.

The field is also younger than the API that publishes it. It was declared and served null for a while before anything populated it, and no backfill filled in the gap, so a message that failed before it was wired reads null however it failed.

Does registering my sender stop carrier filtering?

Registration is the fix for the underlying problem even though it is not a code you will see. For United States long codes it has three levels, each depending on the one before:

  • Brand: the business doing the sending, registered once with your legal details and identity-verified by the registry.
  • Campaign: a declared use case under an approved brand, such as one-time passcodes or order updates, describing what you send and how recipients opt in and out.
  • Number: one of your own US long codes, linked to an approved campaign. Linking is what authorizes that number for the campaign's traffic.

The failure usually sits at the bottom. An approved brand and an approved campaign still leave a number unauthorized until it is linked, and linking authorizes it in addition to the destination allowlist rather than instead of it. 10DLC registration covers each level and its vetting.

Unregistered traffic that a carrier drops still reaches you as content_rejected or unknown, not as sender_unregistered. The registration state is something you check on your own account rather than something the error code tells you.

What actually reduces filtering?

  1. Finish registration and check all three levels, and confirm the destination country is enabled as well.
  2. Treat content_rejected and unknown as the same investigation. Both can be a filter, and the difference between them is only whether the provider's reason mapped.
  3. Log carrier_error_code and branch on code. The normalized code tells your system what to do; the raw one is what support needs to find the cause.
  4. Mirror the suppression list instead of polling it. sms_suppression.created fires when a suppression opens, whether a subscriber texted a stop keyword, a carrier reported an opt-out, or someone added one by hand. One exception is worth knowing: a workspace-wide opt-out recorded on the Preferences tab is a stated preference rather than a suppression and does not fire this event, so mirroring the event alone misses those.

SMS events has the full event catalogue and the payload of each, and opt-outs and keywords covers how a suppression opens.

Bouw op hetzelfde netwerk.

Een test-API-key is direct beschikbaar. Productietoegang wordt ontgrendeld zodra u een betaalmethode toevoegt en een afzender verifieert.

Begin met één kanaal.
Voeg de rest toe wanneer je er klaar voor bent.

Een test-API-key is direct beschikbaar. Productietoegang wordt ontgrendeld zodra je een betaalmethode toevoegt en een afzender verifieert.

Gebruik je Claude Code, Cursor of Codex? Kopieer een setup-prompt en je agent installeert de Bird CLI en skills voor je. Kies de jouwe:

Cursor