Webhooks Lifecycle
This page describes what happens between the moment an event occurs inside Sent and the moment your endpoint receives (or stops receiving) a webhook delivery. Everything below is observable from your side, through the request itself or the delivery history in the Sent Dashboard.
Delivery Lifecycle
The diagram below shows the full lifecycle of a single webhook delivery: from event generation, through HTTP delivery to your endpoint, including retries and the conditions under which a webhook can be auto-disabled.
The same flow drives message, template, channel, contact, and link events; only the envelope contents differ. Filtering by event_types and event_filters is evaluated when the event is generated. Webhooks whose subscription doesn't match the event's sub-type don't receive a delivery attempt.
Each retry uses exponential backoff between attempts. After your configured retry budget is exhausted, the delivery is marked FAILED, and too many consecutive failed delivery attempts auto-disable the webhook. You re-enable it from the Sent Dashboard. The retry schedule, retry budget, and auto-disable threshold are stated canonically in Delivery Statuses & Retries.
End-to-end Message Event Flow
The diagram below shows how a single outbound message moves through its sub-types and the points at which it can branch into message.failed. Inbound events are shown separately because they don't share the outbound state machine.
Not every message travels the whole pipeline. A send held for the recipient's quiet hours, or for a scheduled_at you set, surfaces message.scheduled while it waits, and message.cancelled if you call it off before release. A routing policy such as a recipient opt-out finalizes it as message.filtered, and a gate such as insufficient balance, an unapproved template, or a free-form send with no open conversation finalizes it as message.blocked. FILTERED and BLOCKED are policy decisions rather than delivery failures, so neither counts against your deliverability rate. See Trust & Safety for the gates behind each one.
Not every send produces every event. Channels and carriers vary in how many intermediate statuses they report. For example, some SMS carriers skip straight from routed to delivered. Treat the sub-types as a partial order: if you receive message.delivered, you can safely assume the message was queued, routed, and sent.
Events at a Glance
Sent emits the parent event categories message, templates, channel, contact, link, and call, each with a set of sub-types. The tables below summarise the message, template, channel, link, and call events: what triggers each one, the direction it travels, and where in the lifecycle it can fail. Contact events are catalogued in the Events Reference.
Message events
| Message Events | Direction | SMS | RCS | What it means | Where it can go wrong | |
|---|---|---|---|---|---|---|
QUEUED | Outbound | Yes | Yes | Yes | Sent has accepted the request and is queueing it for routing. | Validation errors (bad number, missing template params) reject the request before this event is emitted. |
ROUTED | Outbound | Yes | Yes | Yes | The message has been assigned to a carrier or channel provider. | Routing can fail if there is no available route to the destination country/channel, which emits message.failed. |
SENT | Outbound | Yes | Yes | Yes | The carrier or provider has accepted the message for delivery. | Carrier rejects the payload (for example, content policy or a blocked number), which emits message.failed. |
DELIVERED | Outbound | Yes | Yes | Yes | The message reached the recipient's device. | Handset offline, number unreachable, or carrier delivery receipt missing, which emits message.failed (or stays at sent until the receipt times out). |
READ | Outbound | No | Yes | Yes | The recipient opened the message. Not emitted for SMS. | Recipient never opens the message, has read receipts turned off, or the channel doesn't support read receipts (SMS). |
FAILED | Outbound | Yes | Yes | Yes | Terminal failure: the message will not be delivered. The payload carries message_status: FAILED with reason_code and reason saying why. | This is the failure event. Branch on payload.reason_code; see the Error Catalog. |
SCHEDULED | Outbound | Yes | Yes | Yes | The send is held until a scheduled_at instant you set, or until the recipient's quiet hours end. | The message is held, not lost; it re-enters the pipeline when the instant arrives or the quiet-hours window closes. |
CANCELLED | Outbound | Yes | Yes | Yes | You called a held send off with POST /v3/messages/{id}/cancel. The payload carries the scheduled_at that was called off. | Terminal: the message is never released or sent, and it cannot be resent. A cancellation after the message is released is refused with 409. |
FILTERED | Outbound | Yes | Yes | Yes | A routing policy (for example, a recipient opt-out) suppressed the message before dispatch. | Terminal for that recipient: the message is not dispatched. |
BLOCKED | Outbound | Yes | Yes | Yes | An account-level gate (for example, insufficient balance or an unmet onboarding entitlement) stopped the message. | Terminal for that message; new sends stay gated until the account condition is resolved. |
RECEIVED | Inbound | Yes | Yes | Yes | A contact replied to one of your numbers (or, on RCS, tapped a quick-reply suggestion chip). | Inbound delivery to your endpoint can still fail at HTTP (see the retry/auto-disable lifecycle in the preceding section). |
Template events
Template events are emitted under the templates parent type as a template moves through review. Each status carries a matching sub-type:
| Template Events | Sub-type | What it means | Where it can go wrong |
|---|---|---|---|
PENDING | templates.pending | The template was submitted for review. A submission covers every channel, so this is a whole-template event and the per-channel results arrive separately. | None |
APPROVED | templates.approved | The leg named in payload.channel is approved and can be sent. | None |
REJECTED | templates.rejected | The leg was rejected. The reason is in payload.reason. | Fix the template content or category, then re-submit. |
PAUSED | templates.paused | Meta paused the WhatsApp leg for low quality. Sends against it are rejected until it recovers. | Terminal for WhatsApp sends while the pause holds. |
DISABLED | templates.disabled | Meta disabled the WhatsApp leg, for example for a policy violation. | That leg is not sendable. Author a replacement rather than appealing in place. |
DELETED, PENDING_DELETION | templates.deleted | The template was deleted, including a deletion made outside Sent. | Sends referencing the template fail after this point. |
CATEGORY_UPDATED | templates.category_updated | The category changed, which changes how the template is priced. Fires when Meta sets or moves it on the WhatsApp leg, and when you change it on an edit, which is the usual way a rejected template is recategorised before resubmitting. See the events reference for the reason strings. | None |
QUALITY_UPDATED | templates.quality_updated | Meta's quality rating for the WhatsApp leg changed. The change is in payload.reason. A declining rating can eventually trigger PAUSED. | None |
Who reviews a template depends on the account. With a connected WhatsApp Business Account, Meta reviews it, and an APPROVED or PENDING verdict applies to every channel while a rejection from Meta applies to the WhatsApp leg only. Without one, Sent's compliance team reviews it and decides each channel individually. Either way payload.channel names the leg a decision is about, a decision covering the whole template omits the field, and an account with no WhatsApp channel keeps a WhatsApp leg sitting at PENDING with nothing to submit it to.
Meta forwards a wider set of lifecycle values than the preceding table covers, and adds to it without notice. A status outside the table arrives on the bare templates parent with no event key and the status reported verbatim in payload.status, so route on field first and treat event as optional.
Channel events
Channel events are emitted under the channel parent type as one market moves through provisioning and compliance. The subject is always one market, named by payload.country:
| Channel Events | Sub-type | What it means | Where it can go wrong |
|---|---|---|---|
| Filed | channel.submitted | Your request is filed and it's with Sent or a registry. Fires for a first filing, for a resubmission, and for a sender ID that Sent pre-registers with the destination's regulator on your behalf once that market's documents are in. | Nothing to do. Registry decisions take days for SMS. |
| Corrections wanted | channel.changes_requested | Something needs correcting before the market can go forward. A market that pre-registers its sender ID arrives here while its required documents are missing. | Nothing progresses until you act. Call GET /v3/compliance/requirements for the market to see which fields and documents are outstanding. |
| Verdict in | channel.approved | The registry answered and the sender is being acquired. For a 10DLC market, this is when the carriers elect your campaign. | Not sendable. A requested area code can extend this to a full acquisition cycle. |
| Turned down | channel.rejected | The sender was refused before it ever went live: The Campaign Registry rejected the campaign, or a sender still being set up was withdrawn. payload.reason says why when one was given. | The market won't go live as it stands. Correct what payload.reason names and resubmit, or request a different sender. |
| Live | channel.activated | The sender is live and the market can carry traffic. Subscribe to this one alone if all you need is to know when you can send. | None. |
| No longer live | channel.deactivated | The sender stopped working: released to the pool, unbound because its campaign lapsed, or cancelled for you. | Stop queueing against the market. Otherwise you learn on the next send. |
payload.status reports where the market stands (PROVISIONING, PENDING_REVIEW, ACTION_NEEDED, ACTIVE, or INACTIVE), and is the same value GET /v3/channels publishes for that market. It answers a different question from the sub-type: the sub-type says what happened, the status says whether the market can send. A resubmission filed against a market whose sender is already live arrives as channel.submitted carrying ACTIVE.
ACTIVE means provisioning and compliance are complete. It does not promise the next send succeeds: a suspended account and a destination blocked by a routing rule both report ACTIVE.
Your organization's webhook receives your organization's own channel events. To receive every Sender Profile's, select channel under sender_profile, which clones the webhook onto each profile, and payload.account_id names the profile whose market moved. See Sender Profile Events.
Link events
Link events are emitted under the link parent type when a tracked short link or a hosted file Sent published on your behalf is used, expires, or is revoked. The subject is always one link, named by payload.record_id:
| Link Events | Sub-type | What it means | Where it can go wrong |
|---|---|---|---|
| Followed | link.clicked | A tracked short link was followed and Sent served the redirect. | Not proof a person opened it. Providers and link scanners fetch URLs on their own, so filter on payload.traffic_class. |
| Served | link.downloaded | A hosted file behind a tracked link was served. | A ranged request reports only the bytes in that range, so one file can produce several events. |
| Expired | link.expired | A published link reached its expiration. | Later requests no longer resolve. Republish rather than expecting the link to recover. |
| Revoked | link.revoked | A published link was permanently revoked. | Terminal for that link. |
This family is opt-in: subscribe to link explicitly, because no other subscription inherits it. Events for a Sender Profile's links go to that profile's webhooks, as every family's do, so select link under sender_profile to receive them through clones of your organization's webhook. Full payload fields are in the Events Reference.
Call events
Call events are emitted under the call parent type as a phone call moves from the moment it starts to the moment it ends, and once more for each recording that becomes available. The subject is always one call, named by payload.call_id:
| Call Events | Sub-type | What it means | Where it can go wrong |
|---|---|---|---|
| Started | call.initiated | The call arrived or was placed. | Sent sends it before its own checks and before your callback is asked, so a call refused for balance, destination or a missing callback URL still has it. Your callback must answer within 2.5 seconds, with one retry, or the call fails with callback_timeout. |
| Answered | call.answered | The call was answered. | A call between two of your app users has no answered_at on its record, so do not wait for this one to time the call. |
| Completed | call.completed | An answered call ended, with its duration_seconds and price. | price is omitted until billing has recorded the charge. Read it from GET /v3/calls/{id} later rather than treating it as zero. |
| Failed | call.failed | The call ended without completing. | reason is omitted when none was recorded. The values are listed under Call Failure Reasons. |
| Recording ready | call.recording_ready | One recording is stored and can be listed, once per recording. | The download links GET /v3/calls/{id}/recordings returns expire. Request the list again for a fresh link rather than storing the URL. |
Full payload fields are in the Events Reference.
What's Next
- Subscribe to the events your app needs in the Events Reference: full payload schemas and filtering rules.
- Verify incoming requests against the signing secret described in Signature Verification.
- See Handling Retries for guidance on idempotency, response codes, and recovery.