Fallback channels
Retry a failed delivery, then try another channel, so an important message is not silently lost.
Retries First
A failed email, SMS, or webhook delivery is first retried under the retry policy (3 attempts by default; see Delivery and retries). A fallback only comes into play once the channel has given up, or could not be used at all. Without a fallback, a delivery that gives up is marked failed, the delivery.failed hook fires, and that is the end of it.
Inbox Fallback
The simplest form is a single inbox item, written when any email, SMS, or webhook delivery fails for good:
notification({
id: "invoice_ready",
payload: { invoiceUrl: "string" },
channels: [email({ subject: "Your invoice is ready", body: "{{invoiceUrl}}" })],
fallback: inbox({ title: "Your invoice is ready", actionUrl: "{{invoiceUrl}}" }),
})It only reacts to failed deliveries, not to missing addresses or preferences, and it is written only if the recipient allows inbox for this notification.
Rules
For more control, pass a list of rules. Each says when it applies and which channel to use:
notification({
id: "security_alert",
payload: { message: "string" },
channels: [email({ subject: "Security alert", body: "{{message}}" })],
fallback: [
{
if: "missing_address",
from: "email",
then: sms({ body: "{{message}}" }),
},
{
if: "channel.failed",
from: "email",
then: inbox({ title: "Security alert" }),
},
],
})| Field | What it is |
|---|---|
if | The trigger, below |
then | A channel config, built with the same factories as the notification's channels |
from | Optional. Only apply when this channel type triggered it |
| Trigger | Fires when |
|---|---|
channel.failed | A delivery failed after all its retries |
missing_address | The recipient has no email for an email channel, or no phone for SMS |
skipped | The recipient's preferences or your defaults turned the channel off |
Rules are checked in order and the first match wins. A rule only matches if its then channel type is not one the notification already has, or one an earlier fallback already used, so a fallback always reaches the recipient somewhere new. If every channel is turned off but a skipped or missing_address rule matches, the send goes ahead for the fallback instead of being suppressed.
How Fallbacks Behave
- One step only. If the fallback delivery itself fails, no further fallback is tried. There is no email, then SMS, then inbox cascade; put your most reliable channel in
then. - Preferences still apply. The fallback channel is checked like any other: if the recipient turned that channel off, or has no address for it, nothing is sent. A
requirednotification overrides preferences here too. - Quiet hours do not. A fallback email, SMS, or webhook is sent right away, even inside the recipient’s quiet hours.
- Retries apply. A fallback delivery gets the same retry policy.
- Timing depends on the trigger.
missing_addressandskippedfallbacks run insidesend(). Achannel.failedfallback runs after the last retry, which with an asynchronous queue is aftersend()has returned.
Startup Checks
Fallbacks are validated when createNotifyKit() runs. It throws if a fallback targets a channel with no provider configured, uses an unknown trigger or channel type, or references a template variable missing from the payload. It warns if the list is empty, if from names a channel the notification does not have (the rule can never match), or if from and then are the same type.
Writing Fallback Content
- Keep the action. If the email had a link, give the inbox item the same
actionUrl, so the recipient can still act. - Fit the channel. An SMS fallback for a long email should be a short line and a link, not the whole body.
- Do not mention the failure. “We couldn’t email you” is noise; say what the message was going to say.
Debugging
A fallback that ran leaves a fallback.triggered event in the timeline, naming the trigger and the channel it came from. If one did not fire, check in this order:
- Did the primary channel actually fail, or was it held, deduplicated, or rate-limited?
- Does a rule match that trigger, and its
from, if set? - Is the
thenchannel type already one of the notification’s channels? - Does the recipient allow that channel, and have an address for it?
In tests, the fake providers can fail on demand: fakeEmailProvider({ failOnNext: true }), or setFailOnNext(true) on an existing one. Set retry: { maxAttempts: 1 } so the failure is final. See Testing and dev mode.