get-convex / get-convex/resend
email.suppressed webhook events are dropped (not in ACCEPTED_EVENT_TYPES / vEmailEvent), leaving email state stale
- Dominant language
- TypeScript
- Stars
- 39
- Forks
- 25
- Avg merge
- 2d 13h
- Merged PRs (30d)
- 2
Description
### Summary
Resend's documented [`email.suppressed` webhook event](https://resend.com/docs/dashboard/webhooks/event-types) is rejected by the component's webhook handler, so applications never learn that a send was blocked by Resend's suppression list. The email's status is never updated and the app-level `onEmailEvent` callback is never invoked for it.
### Where it happens
`src/component/shared.ts` — `email.suppressed` is missing from both the event validator union and the accepted list:
```ts
export const ACCEPTED_EVENT_TYPES = [
"email.sent",
"email.delivered",
"email.bounced",
"email.complained",
"email.failed",
"email.delivery_delayed",
"email.opened",
"email.clicked",
] as const;
```
`vEmailEvent` (same file) is a union of the same types, so in `handleEmailEvent` (`src/component/lib.ts`) the parse fails and the event is dropped with only a warning:
```ts
const result = attemptToParse(vEmailEvent, args.event);
if (result.kind === "error") {
console.warn(
`Invalid email event received. You might want to to exclude this event from your Resend webhook settings in the Resend dashboard. ${result.error}.`,
);
return;
}
```
The warning even suggests excluding the event in the Resend dashboard — but `email.suppressed` is a legitimate terminal outcome for an email, not noise.
### Impact
When Resend suppresses a send (recipient on the account-level suppression list), the email's component status stays at its last known state and downstream apps that track message state via `onEmailEvent` show the message stuck (e.g. permanently "queued"/"sent"), with no signal that delivery will never happen. There's also no way to feed suppression back into an app's own consent/suppression handling.
### Suggested fix
- Add `email.suppressed` to `ACCEPTED_EVENT_TYPES` and to the `vEmailEvent` union (payload appears to carry the common fields; docs page: https://resend.com/docs/webhooks/emails/suppressed).
- Treat it as a terminal, non-retryable state in `computeEmailUpdateFromEvent` (similar to `email.failed`/`email.bounced`), so `finalizedAt`-style semantics and the `onEmailEvent` callback both fire.
Happy to open a PR if the maintainers agree on the desired status semantics.
**Observed with:** `@convex-dev/resend` 0.2.6 (latest); `main` at time of filing has the same `ACCEPTED_EVENT_TYPES`.
Contributor guide
Research direction
Start in src/component/shared.ts by comparing ACCEPTED_EVENT_TYPES and vEmailEvent with Resend's email.suppressed payload, then follow handleEmailEvent in src/component/lib.ts into computeEmailUpdateFromEvent. Done means the event is accepted, treated as a terminal non-retryable outcome like failed or bounced, updates email state, and invokes onEmailEvent.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100