Webhooks

How Send & Retain receives delivery, bounce, and complaint events from your provider.

Send & Retain receives webhooks from your email provider (Resend or SendGrid) to track delivery, opens, clicks, bounces, and complaints. You don't build these endpoints — they're part of the platform. This page explains what they do so you can reason about message status and suppression.

Each project has its own webhook URL and signing secret, registered when you connect a provider:

https://sendandretain.com/api/webhooks/resend/{projectId}
https://sendandretain.com/api/webhooks/sendgrid/{projectId}

Verification

Every webhook is verified against the raw request body before it's trusted. An unknown project id returns 404; a bad signature returns 401.

Resend signs with Svix. The signature is an HMAC-SHA256 over {svix-id}.{svix-timestamp}.{body}, keyed with the project's whsec_… secret, sent in svix-id, svix-timestamp, and svix-signature headers. Timestamps outside a 5-minute window are rejected. Events are de-duplicated on svix-id.

SendGrid signs with ECDSA (P-256 / SHA-256) over {timestamp}{body}, using the project's public key. The signature is in X-Twilio-Email-Event-Webhook-Signature and the timestamp in X-Twilio-Email-Event-Webhook-Timestamp. A SendGrid payload is a JSON array of events (batched, possibly out of order); events are de-duplicated on sg_event_id.

Event types

Provider events are normalized to a common set. The most important mappings:

NormalizedResendSendGrid
sentemail.sent
deliveredemail.delivereddelivered
delivery_delayedemail.delivery_delayeddeferred
openedemail.openedopen
clickedemail.clickedclick
bouncedemail.bouncedbounce, blocked (soft)
complainedemail.complainedspamreport
failedemail.faileddropped
canceledemail.canceled
unsubscribedunsubscribe

Provider events outside this set (Resend email.scheduled, SendGrid processed) are ignored rather than recorded.

Status ladder

A message's status only ever moves forward along this ladder, so out-of-order webhooks can't regress it:

queued → scheduled → sending → sent → delivered → bounced → complained

failed, canceled, and suppressed are terminal. opened, clicked, unsubscribed, and delivery_delayed are recorded on the timeline but don't change the status. You can read the full timeline via GET /api/v1/emails/{id}.

Auto-suppression

Hard bounces and complaints automatically add the recipient to the project's suppression list, so you can't keep sending to an address that bounced or reported spam:

  • A complaint suppresses the recipient (reason complaint) — and complaint suppressions are never removable.
  • A permanent bounce suppresses the recipient (reason bounce).
  • SendGrid unsubscribe events also suppress (reason unsubscribe).

Suppression reasons have a severity order — complaint > bounce > manual > unsubscribe — so a stronger reason is never downgraded by a weaker one.

On this page