When to use
Use this when you need to know what actually happened to mail after the API accepted it — delivered, bounced, opened, clicked, spam-reported, unsubscribed — by receiving and processing the SendGrid Event Webhook.
Not for: receiving replies or inbound mail (see twilio-sendgrid-inbound-parse) or interpreting the aggregate engagement picture (see twilio-sendgrid-engagement-quality).
Method
- Expose an HTTPS endpoint that accepts
POST with a JSON array of event objects. SendGrid batches many events per request.
- Enable the Event Webhook in Mail Settings / the Event Webhook config, select event categories, and point it at your URL. Enable Signed Event Webhook to get a public verification key.
- Verify every request before trusting it. SendGrid signs with ECDSA: read
X-Twilio-Email-Event-Webhook-Signature and X-Twilio-Email-Event-Webhook-Timestamp, concatenate timestamp + raw_body, and verify against your stored public key. Reject on mismatch.
- Process idempotently. Each event has
sg_event_id (unique) and sg_message_id (ties to the send). De-duplicate on sg_event_id; you will get retries and occasional duplicates.
- Map event types to state:
processed, delivered, deferred, bounce, dropped, blocked, open, click, spamreport, unsubscribe, group_unsubscribe, group_resubscribe. Bounce/dropped/spamreport should update your own suppression/reputation state.
- Respond fast with
2xx, then process async. Slow endpoints cause SendGrid to retry and eventually disable the webhook.
- Preserve
category and custom_args from events to attribute outcomes back to your records.
Example
Verify the signature (Node, using the raw request body):
const { EventWebhook } = require('@sendgrid/eventwebhook');
const ew = new EventWebhook();
const key = ew.convertPublicKeyToECDSA(process.env.SG_WEBHOOK_PUBKEY);
app.post('/sg/events', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.get('X-Twilio-Email-Event-Webhook-Signature');
const ts = req.get('X-Twilio-Email-Event-Webhook-Timestamp');
if (!ew.verifySignature(key, req.body, sig, ts)) return res.sendStatus(403);
const events = JSON.parse(req.body.toString());
res.sendStatus(200); // ack first
for (const e of events) enqueue(e.sg_event_id, e); // dedupe + async
});
Pitfalls
- Verifying against a parsed body. ECDSA verification needs the exact raw bytes. If a JSON middleware reparses/reserializes first, verification fails. Capture the raw body.
- No idempotency. Retries and duplicates double-count opens or re-suppress addresses. De-dupe on
sg_event_id.
- Slow synchronous handlers. Doing DB work before responding leads to timeouts, retries, and auto-disable. Ack
2xx, then process.
- Trusting unsigned events. An open endpoint can be spoofed to forge unsubscribes/bounces. Always verify the signature.
- Ignoring
deferred vs bounce. Deferred is temporary (retrying); bounce is terminal. Only suppress on hard bounce/dropped/spamreport.
Output format
# Event Webhook: <endpoint>
CONFIG: url=<https...> events=[delivered,bounce,open,click,spamreport,unsubscribe]
SIGNED: yes pubkey=<vault ref>
VERIFY: raw-body + timestamp -> ECDSA check -> 403 on fail
IDEMPOTENCY: dedupe key = sg_event_id
STATE MAP: bounce/dropped/spamreport -> suppress; open/click -> engagement
ACK: 2xx immediate, async processing
Reference
- Event Webhook: JSON array
POST to your URL; configured via Event Webhook settings (/v3/user/webhooks/event/settings).
- Signature headers:
X-Twilio-Email-Event-Webhook-Signature, X-Twilio-Email-Event-Webhook-Timestamp; signed payload = timestamp + raw_body, verified with the ECDSA public key from settings. SDK helper: @sendgrid/eventwebhook.
- Event fields:
event, email, timestamp, sg_event_id, sg_message_id, category, plus reason/status/url depending on type.
- Deliverability: bounce, dropped, and spamreport events are the ground truth that should feed your suppression lists and reputation monitoring.
1---2name: twilio-sendgrid-webhooks3description: Consume the SendGrid Event Webhook — configure it, verify the ECDSA signature, handle each event type idempotently, and turn delivery/engagement events into reliable state.4---56## When to use78Use this when you need to know what actually happened to mail after the API accepted it — delivered, bounced, opened, clicked, spam-reported, unsubscribed — by receiving and processing the SendGrid Event Webhook.910**Not for:** receiving replies or inbound mail (see `twilio-sendgrid-inbound-parse`) or interpreting the aggregate engagement picture (see `twilio-sendgrid-engagement-quality`).1112## Method13141. Expose an HTTPS endpoint that accepts `POST` with a JSON array of event objects. SendGrid batches many events per request.152. Enable the Event Webhook in Mail Settings / the Event Webhook config, select event categories, and point it at your URL. Enable Signed Event Webhook to get a public verification key.163. Verify every request before trusting it. SendGrid signs with ECDSA: read `X-Twilio-Email-Event-Webhook-Signature` and `X-Twilio-Email-Event-Webhook-Timestamp`, concatenate `timestamp + raw_body`, and verify against your stored public key. Reject on mismatch.174. Process idempotently. Each event has `sg_event_id` (unique) and `sg_message_id` (ties to the send). De-duplicate on `sg_event_id`; you will get retries and occasional duplicates.185. Map event types to state: `processed`, `delivered`, `deferred`, `bounce`, `dropped`, `blocked`, `open`, `click`, `spamreport`, `unsubscribe`, `group_unsubscribe`, `group_resubscribe`. Bounce/dropped/spamreport should update your own suppression/reputation state.196. Respond fast with `2xx`, then process async. Slow endpoints cause SendGrid to retry and eventually disable the webhook.207. Preserve `category` and `custom_args` from events to attribute outcomes back to your records.2122## Example2324Verify the signature (Node, using the raw request body):2526```js27const { EventWebhook } = require('@sendgrid/eventwebhook');28const ew = new EventWebhook();29const key = ew.convertPublicKeyToECDSA(process.env.SG_WEBHOOK_PUBKEY);3031app.post('/sg/events', express.raw({ type: '*/*' }), (req, res) => {32 const sig = req.get('X-Twilio-Email-Event-Webhook-Signature');33 const ts = req.get('X-Twilio-Email-Event-Webhook-Timestamp');34 if (!ew.verifySignature(key, req.body, sig, ts)) return res.sendStatus(403);35 const events = JSON.parse(req.body.toString());36 res.sendStatus(200); // ack first37 for (const e of events) enqueue(e.sg_event_id, e); // dedupe + async38});39```4041## Pitfalls4243- **Verifying against a parsed body.** ECDSA verification needs the exact raw bytes. If a JSON middleware reparses/reserializes first, verification fails. Capture the raw body.44- **No idempotency.** Retries and duplicates double-count opens or re-suppress addresses. De-dupe on `sg_event_id`.45- **Slow synchronous handlers.** Doing DB work before responding leads to timeouts, retries, and auto-disable. Ack `2xx`, then process.46- **Trusting unsigned events.** An open endpoint can be spoofed to forge unsubscribes/bounces. Always verify the signature.47- **Ignoring `deferred` vs `bounce`.** Deferred is temporary (retrying); bounce is terminal. Only suppress on hard bounce/dropped/spamreport.4849## Output format5051```52# Event Webhook: <endpoint>53CONFIG: url=<https...> events=[delivered,bounce,open,click,spamreport,unsubscribe]54SIGNED: yes pubkey=<vault ref>55VERIFY: raw-body + timestamp -> ECDSA check -> 403 on fail56IDEMPOTENCY: dedupe key = sg_event_id57STATE MAP: bounce/dropped/spamreport -> suppress; open/click -> engagement58ACK: 2xx immediate, async processing59```6061## Reference6263- Event Webhook: JSON array `POST` to your URL; configured via Event Webhook settings (`/v3/user/webhooks/event/settings`).64- Signature headers: `X-Twilio-Email-Event-Webhook-Signature`, `X-Twilio-Email-Event-Webhook-Timestamp`; signed payload = `timestamp + raw_body`, verified with the ECDSA public key from settings. SDK helper: `@sendgrid/eventwebhook`.65- Event fields: `event`, `email`, `timestamp`, `sg_event_id`, `sg_message_id`, `category`, plus `reason`/`status`/`url` depending on type.66- Deliverability: bounce, dropped, and spamreport events are the ground truth that should feed your suppression lists and reputation monitoring.