How to Handle the New RCS Error Code 30046 in Your WhatsApp Bot with Twilio
If you have a bot running on Twilio — whether for WhatsApp, RCS, or both — and you don't update your error handling before August 10, 2026, you're going to wake up to dropped messages, retries that never fire, and ghost users who never received anything. This isn't a minor heads-up: it's a production behavior change that breaks existing logic if you have 30036 hardcoded anywhere.
This tutorial walks you through identifying where the risk lives in your code, updating your handlers correctly, and getting your system ready for the new 30046 error code — the one Twilio will use starting that date when an RCS message cannot be delivered because the destination device is unavailable or doesn't support RCS.
Context: What Changed and Why It Matters
Twilio uses RCS (Rich Communication Services) as an alternate or primary channel for some markets. When an RCS message can't be delivered — because the device doesn't have RCS enabled, is powered off, or simply isn't reachable — Twilio returns an error code in the message status webhook.
Until now, that code was 30036. Starting August 10, 2026, that same scenario returns 30046.
The code 30036 doesn't disappear completely for all cases, but the specific case of "RCS unreachable device" migrates to 30046. If your bot listens to status webhooks and makes decisions based on that code (like falling back to SMS or WhatsApp), you need to update that logic now.
Prerequisites
- Active Twilio account with RCS enabled (or in the process of enabling it)
- Bot with message status webhook configured (
statusCallback) - Node.js 20+ or Python 3.11+ (examples cover both)
- Access to your current webhook logs to identify if you're already receiving 30036
Step 1: Audit Your Current Code
Search your entire project for references to code 30036. In bash:
grep -r "30036" ./src
Also search for variants as strings, numbers in switches, or comparisons:
grep -rE "(30036|'30036'|\"30036\")" ./src
If you find zero results and you have no error handling by numeric code at all, you have a different problem: you're not handling delivery errors at all. Keep reading anyway.
Step 2: Understand the Status Webhook Structure
When Twilio attempts to deliver a message and fails, it sends a POST to your statusCallback with a payload like this:
{
"MessageSid": "SMxxxxxxxxxxxxxxxx",
"MessageStatus": "failed",
"ErrorCode": "30036",
"ErrorMessage": "Message delivery failed: unreachable destination handset",
"To": "whatsapp:+521234567890",
"From": "whatsapp:+14155238886"
}
Starting August 10, that ErrorCode will be "30046" for the RCS unreachable device case. The ErrorMessage field will also change to reflect the new code.
Step 3: Update Your Webhook Handler
In Node.js (Express):
// webhook/statusCallback.js
const TWILIO_RCS_UNREACHABLE_CODES = new Set(['30046', '30036']); // keep both during transition
app.post('/webhook/status', (req, res) => {
const { MessageSid, MessageStatus, ErrorCode, To } = req.body;
if (MessageStatus === 'failed') {
if (TWILIO_RCS_UNREACHABLE_CODES.has(ErrorCode)) {
console.warn(`[RCS_UNREACHABLE] SID: ${MessageSid} | To: ${To} | Code: ${ErrorCode}`);
// Fallback logic: try via WhatsApp or SMS
handleRCSFallback({ messageSid: MessageSid, destination: To });
} else {
console.error(`[MSG_FAILED] SID: ${MessageSid} | Code: ${ErrorCode}`);
// Other errors: log, alert, retry per policy
}
}
res.sendStatus(200);
});
In Python (FastAPI):
# routes/status_callback.py
from fastapi import APIRouter, Request
import logging
router = APIRouter()
TWILIO_RCS_UNREACHABLE_CODES = {"30046", "30036"} # both during transition
@router.post("/webhook/status")
async def status_callback(request: Request):
form = await request.form()
message_sid = form.get("MessageSid")
status = form.get("MessageStatus")
error_code = form.get("ErrorCode")
destination = form.get("To")
if status == "failed":
if error_code in TWILIO_RCS_UNREACHABLE_CODES:
logging.warning(f"[RCS_UNREACHABLE] SID: {message_sid} | To: {destination} | Code: {error_code}")
await handle_rcs_fallback(message_sid, destination)
else:
logging.error(f"[MSG_FAILED] SID: {message_sid} | Code: {error_code}")
return {"status": "ok"}
Important note: Keep both codes (
30036and30046) in your handled codes set during the transition period. Twilio doesn't guarantee the switch will be instantaneous for all accounts exactly on August 10 — there may be a margin of hours. Remove30036from the RCS unreachable set a couple of weeks after you've confirmed the change in your logs.
Step 4: Implement the Fallback Logic
If you receive a 30046, the most common response is to attempt the same message through an alternate channel. Here's an example with WhatsApp fallback:
// services/fallback.js
const twilio = require('twilio');
const client = twilio(process.env.TWILIO_ACCOUNT_SID, process.env.TWILIO_AUTH_TOKEN);
async function handleRCSFallback({ messageSid, destination }) {
// Retrieve original message from your DB using the SID
const originalMessage = await db.messages.findOne({ twilioSid: messageSid });
if (!originalMessage) {
console.error(`Original message not found for SID: ${messageSid}`);
return;
}
// Try via WhatsApp
try {
const fallbackMsg = await client.messages.create({
from: 'whatsapp:+14155238886',
to: destination.startsWith('whatsapp:') ? destination : `whatsapp:${destination}`,
body: originalMessage.body,
});
console.log(`[FALLBACK_OK] New SID: ${fallbackMsg.sid}`);
await db.messages.update({ twilioSid: messageSid }, { fallbackSid: fallbackMsg.sid, fallbackChannel: 'whatsapp' });
} catch (err) {
console.error(`[FALLBACK_FAIL] ${err.message}`);
// Here you can queue for SMS or notify the team
}
}
Step 5: Update Your Alerts and Dashboards
If you have alerts in Datadog, Grafana, Sentry, or any observability tool configured with 30036 as a filter, update them now. An alert filtering by ErrorCode == 30036 will stop firing for the unreachable device scenario after the change.
Create a new alert for 30046 or, better yet, one that covers the relevant range of Twilio error codes for your operation.
Step 6: Test in Staging Before August 10
Twilio doesn't offer a native simulator to force a 30046 error in sandbox, but you can:
- Send to a number that doesn't exist or that you've confirmed has no RCS enabled
- Mock the status webhook in your test environment with a payload that includes
"ErrorCode": "30046" - Use a tool like
ngrok+ a test script to fire your handler manually
curl -X POST https://your-ngrok.ngrok.io/webhook/status \
-d "MessageSid=SMtest123" \
-d "MessageStatus=failed" \
-d "ErrorCode=30046" \
-d "ErrorMessage=Message delivery failed: unreachable destination handset" \
-d "To=whatsapp:+521234567890" \
-d "From=whatsapp:+14155238886"
Confirm your handler responds with 200 and that the fallback logic executes correctly.
Production Warnings
- Don't delete 30036 handling immediately. There are other scenarios where that code may still appear. Review Twilio's documentation for cases that remain under 30036.
- Idempotency in the fallback. Your fallback function must verify whether a fallback was already sent for that SID before creating a new one. Twilio webhooks can arrive duplicated.
- Rate limits. If you have a high volume of RCS failed messages at the same time, your fallback logic can hit Twilio's rate limits. Implement a queue with exponential backoff.
- Structured logging. Log the
ErrorCodeas a separate field, not concatenated in a string. That lets you filter and group in your observability system without regex.
Change Summary
| Scenario | Previous Code | New Code (from Aug 10, 2026) |
|---|---|---|
| RCS unreachable device | 30036 | 30046 |
| Other delivery errors | various | no change |
Update, test, and sleep easy. The change is small but the production impact can be significant if you ignore it.