Skip to main content

Troubleshooting: WhatsApp 24-hour session expired

A WhatsApp free-form send is refused with WHATSAPP_SESSION_EXPIRED when Meta’s 24-hour customer-service window for the recipient has closed. The window opens when the recipient messages you and stays open for 24 hours after their last inbound message; outside it, WhatsApp accepts template messages only — any session (free-form) send is refused either by Orbit before dispatch or by Meta further down the same chain (WHATSAPP_RE_ENGAGEMENT_WINDOW_CLOSED / WHATSAPP_RE_ENGAGEMENT_REQUIRED, then WHATSAPP_OUTSIDE_24H_WINDOW). The full window model is in the 24-hour window guide; this page is the fix runbook for the error row you hit. The gate is Meta-side and tenant-owned: it clears when the recipient replies (a reply re-opens the window) or when you send an approved template. Retrying the same free-form body changes nothing — pick the remediation below instead. The code family sits in the Error Code Reference.

What the rejection looks like

The refusal comes back on the send call with the code on the error envelope, and the same shape appears on failed rows in the Delivery Log, on campaign recipient failures, and on message.failed webhook payloads:
This gate is not a throttle: there is no retry_after in seconds the way RATE_LIMITED carries one. No amount of waiting with the same free-form body re-opens the window — only a recipient reply re-opens it. Time helps exactly one way: waiting for the recipient to answer. Until then, template messaging is the only path through. How re-engagement works is the subject of the 24-hour window guide.

Root causes

The window-status endpoint and its reason values (within_window, window_expired, no_inbound_history, unknown) are documented on the WhatsApp channel page.

Fixes

1. Gate the UI with the window-status check

Before you let an agent or integration compose a free-form message, check whether the recipient is still inside the window:
When the response reports within_window: false, switch the composer to template-only instead of letting the send fail. The send pipeline enforces the same check at dispatch; the pre-flight exists so the UI can prevent the wasted attempt.

2. Fall back to an approved template

Outside the window, the only permissible content is an approved template. Resend the substantive content as a template message, then treat the recipient’s reply as the trigger to resume free-form:
If the template refuses because it has no variant for WhatsApp, author the missing variant per the template-variant-missing runbook; for rejected templates, work through the WhatsApp template troubleshooting page. Template management and approval flow are under Channels → WhatsApp → Templates and on the WhatsApp channel page.

3. Re-engage the recipient

Once the recipient replies to anything — a template, an earlier free-form message, even a reaction — the window re-opens and free-form is accepted again. Structure re-engagement templates so a reply comes naturally, and time follow-ups inside the window rather than batching them hours later.

What not to do

  • Do not retry the free-form send in a tight loop. Every retry fails identically until the recipient replies — the error is deterministic, not transient, and the retry storm only pollutes your delivery metrics.
  • Do not treat this as a rate limit. Backing off for a few seconds and resending is the wrong tool; there is no cooldown to wait out. Re-route: send a template, or move the content to a different channel with a cascade (SMS or Email under one message_group_id — see Cascade failover policy).
  • Do not send the same content on every channel at once. Cascade fallback is ordered and conditional; a blast on every channel is spam the recipient can report, which degrades your WABA quality rating.

Escalation criteria

Open a ticket with support@devotel.io when the window-status pre-flight reports within_window: true and the free-form send is still refused with this code, or when refusals persist across every recipient rather than the lapsed ones. Include:
  • The request_id from meta on the refusal.
  • The to and from pair exactly as sent.
  • The window-status response (endpoint output above) showing the mismatch.
If the refusal code moved between window-status and the send, that race (recipient replied between check and send, or last-inbound aged out mid-flight) is expected — re-check and the state will agree.

See also