Why didn’t it arrive?
Every delivery that was not sent carries a reason. Find it, then find it here: what it means and what to do.
1
Find the reason
- Console: Notify › Recipients, open the user, and read their timeline: every delivery to them across notifications, with its state and reason. Explain for recipient runs a dry run of a type for that user and shows each channel’s decision now.
- API:
GET /deliveries?notification_id=…orGET /recipients/{external_id}/timeline; each delivery hasstate,reasonandexplanation. Before sending,dry_run: trueon the send returns the plan with the same reasons. - Webhooks:
delivery.skippedanddelivery.failedcarry the reason. - An AI client: the MCP server’s explain tool answers with the fix below.
Nothing in the log at all? The send call failed before it was stored: check its HTTP status and error code on the error codes page. A recipient listed in the response’s skipped was left out of an otherwise accepted send.
A push shows sent but the user saw nothing? Apple and Google only report that they accepted it. Common causes on the device: notifications turned off for the app in system settings, Focus or Do Not Disturb, a sandbox token from a debug build (see iOS push), an Android notification channel the app never created, or the app being force-stopped on Android. The rendered text and Apple’s apns-id or Google’s message name are on the delivery, for Apple’s Push Notifications Console or Firebase.
2
It has not arrived yet: it is waiting
The delivery is in state scheduled or retry. Nothing is lost; it goes out on its own.
| Reason | What happened | What to do |
|---|---|---|
held_quiet_hours | Held until the recipient's quiet hours end. | It is sent when the window ends. Mark the type critical if it must not wait. |
scheduled_send | Waiting for the send time given in send_at or delay_seconds. | It is sent at that time. Cancel it with the notification id or its cancellation_key. |
awaiting_unread | A follow-up step waiting to see whether the recipient reads the inbox item first. | Nothing to fix: it is sent when the after_unread_seconds timer ends and the item is still unread, and skipped as read_in_inbox otherwise. |
email_budget | The workspace email budget is spent for now; the email waits for the next window. | It is sent automatically when the budget allows. Ask us to raise the budget if this is routine. |
circuit_open | The provider is failing for this credential, so sends are paused briefly and retried. | Nothing to fix: it retries on its own. Check the provider's status page if it lasts. |
3
The recipient chose not to get it
Skipped on purpose. Critical types still reach recipients who opted out (but never past bounces, complaints or Mail suppression).
| Reason | What happened | What to do |
|---|---|---|
suppressed_preference | The recipient turned this channel off for this type or its category. | Nothing to fix: the recipient chose this. Mark the category channel mandatory only for messages they must receive. |
unsubscribed_all | The recipient unsubscribed from all non-critical notifications. | Only types marked critical still reach them; otherwise the recipient has to opt back in. |
read_in_inbox | The recipient read the inbox item first, so the follow-up was not needed. | Nothing to fix: this is how after_unread_seconds works. |
actor_excluded | The recipient is the actor who caused the notification. | Pass exclude_actor: false to notify the actor too. |
4
There was nowhere to send it
Notify had no device, address or recipient for this channel. A fallback step, if the type has one, runs next.
| Reason | What happened | What to do |
|---|---|---|
no_device | The recipient has no active device for this channel. | Register the device token from your app (POST …/devices) after the user grants permission. |
no_address | The recipient has no email address. | Set email on the recipient, or pass it inline in the send. |
pruned | The push provider said the device token is no longer valid; the device was removed. | Nothing to fix: the app registers a fresh token the next time it opens. |
recipient_not_found | No recipient with this external id exists in the workspace. | Create the recipient first (PUT /recipients/{external_id}), pass it inline with an email, or send create_missing: true. |
recipient_erased | The recipient was erased in the last 24 hours and cannot be recreated by a send yet. | Wait 24 hours after the erasure, then create the recipient again. |
5
Your setup is missing something
Fix it once in the console or the API and the next send goes through.
| Reason | What happened | What to do |
|---|---|---|
channel_not_configured | The channel is not set up in this workspace. | Upload APNs or FCM credentials, generate web push keys, or enable Mail with a verified sender. |
credential_invalid | No valid credential matches this device's app or environment. | Upload the APNs key or FCM service account for this bundle id or project, in the right environment. |
credential_error | The push provider rejected the workspace credential. | Upload a fresh APNs key or FCM service account; the channel resumes as soon as it validates. |
template_missing | The type has no template for this channel. | Add a template for the channel (any locale) to the notification type. |
type_archived | The notification type is archived. | Unarchive the type or send a different one. |
channel_restricted | The send's channels list left this channel out. | Add the channel to channels, or omit channels to use every channel of the type. |
6
Email protection stopped it
These keep your sender reputation (and every other customer’s) intact. Critical types do not bypass them.
| Reason | What happened | What to do |
|---|---|---|
email_invalid | An earlier email to this address hard-bounced. | Update the recipient's email address; a new address clears the bounce. |
email_complained | The recipient marked an earlier email as spam. | Email stays off for this recipient; reach them through another channel. |
mail_suppressed | ProductCraft Mail suppressed the address (earlier bounces or complaints). | Check the suppression list in Mail; remove the address only if you know it is valid. |
mail_refused:<code> | ProductCraft Mail refused the message (the code after the colon says why). | Look up the Mail error code; usually the template name or sender needs fixing. |
email_paused | Email is paused for this workspace (high bounce or complaint rate). | Check your sending in the console and resume email in Notify settings. |
email_backlog_full | The workspace already holds 10,000 emails waiting for budget, so this one was not queued. | Send fewer emails or spread them out; ask us to raise the budget if this volume is routine. |
7
A limit stopped it
Per-recipient budgets and per-type throttles protect your users from floods; the excess is skipped, not queued.
| Reason | What happened | What to do |
|---|---|---|
rate_limited | The recipient already received the per-minute maximum of notifications. | Send fewer notifications to one person, or batch them; critical types have their own budget. |
throttled | The type's own throttle for this recipient was reached. | Raise or remove throttle on the notification type if every message must go out. |
payload_too_large | The rendered push payload is over the provider's size limit. | Shorten the template or move data out of the push payload. |
8
Delivery ended without success
The delivery is final. The attempts in the message log show what the provider answered.
| Reason | What happened | What to do |
|---|---|---|
expired | The notification's time to live passed before it could be delivered. | Raise ttl_seconds on the type if late delivery is still useful. |
cancelled | The notification was cancelled, or the recipient was erased, before delivery. | Nothing to fix. |
provider_error | The provider kept failing until retries ran out. | Check the attempts for the provider status and code. |