A user says "I never got my sign-in link". Your logs show:
422 Unprocessable Entity
ErrorCode: 406
Message: You tried to send to recipient(s) that have been marked as inactive.
What it means
Postmark keeps a suppression list per message stream. An address lands on it after:
- a hard bounce (the mailbox does not exist),
- a spam complaint (the person clicked "report spam"),
- a manual suppression (you, support, or an unsubscribe on a broadcast stream).
After that, Postmark refuses every send to that address on that stream. It does this to protect your reputation. The message was never sent.
What not to do
- Do not retry. It is a 422. It fails the same way every time.
- Do not reactivate it to make the error go away. A dead mailbox bounces again. A complainer complains again, and complaints cost more than bounces.
- Do not switch streams to get around it. Sending a login link on the broadcast stream "because it's not blocked there" puts transactional mail on marketing infrastructure with an unsubscribe footer.
What to do
- Turn it into a clear domain error. The Node SDK throws
Errors.InactiveRecipientsErrorfor this case. Catch it at the send boundary and throw your ownEmailSuppressedError(address). - Show the user something useful. "We can't deliver email to this address. Check it, or use a different one." Not "something went wrong".
- Find out why. Search the address in Postmark (Suppressions tab on the stream), or query the API:
const { Suppressions } = await client.getSuppressions("outbound", { emailAddress });
// [{ EmailAddress, SuppressionReason: "HardBounce" | "SpamComplaint" | "ManualSuppression", Origin, CreatedAt }]
- Reactivate only on request. If the owner fixed their mailbox and asks, delete the suppression:
await client.deleteSuppressions("outbound", { Suppressions: [{ EmailAddress: emailAddress }] });
SpamComplaint suppressions cannot be deleted through the API. The person reported you, so they have to write to you (or to Postmark support) to undo it.
Refuse before you call
A 406 costs an API round trip and happens after you rendered the email. Keep a local copy of the list, filled by the Bounce, SpamComplaint and SubscriptionChange webhooks, and check it first. Treat the local list as a fast path, not the authority:
- If the local check fails (database down), let the send through. Postmark still refuses with a 406.
- If Postmark reactivates an address (a SubscriptionChange with
SuppressSending: false), remove it locally too.
Checklist
- [ ]
InactiveRecipientsErroris mapped to a domain error, not reported as a crash. - [ ] No retry on any 422.
- [ ] Users see an actionable message.
- [ ] Reactivation is a support action on request, never automatic.