Polling the ESP's message status API on a timer introduces latency (you learn about a hard bounce minutes later, not seconds), consumes API quota on every poll cycle, and misses events that occur between polling intervals. Gmail and Yahoo's 2024 bulk sender requirements mandate one-click unsubscribe processing — a webhook-less implementation cannot honor those events promptly. CWE-345 covers insufficient verification of data authenticity; a webhook without HMAC signature verification lets anyone inject fake delivery events and manipulate your suppression list.
High because missing webhook processing means delivery failures, bounces, and unsubscribe events are never recorded, leading to repeated sends to invalid addresses and sender reputation damage.
Register a webhook endpoint at app/api/webhooks/sendgrid/route.ts (Next.js App Router) that verifies the HMAC signature before processing events:
const signature = req.headers.get('x-twilio-email-event-webhook-signature') ?? ''
const timestamp = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? ''
const payload = timestamp + await req.text()
const computed = crypto.createHmac('sha256', process.env.SENDGRID_WEBHOOK_SECRET!)
.update(payload).digest('base64')
if (computed !== signature) return new Response('Invalid signature', { status: 401 })
Process at minimum delivered, bounce, and spamreport event types and update your emailSendLog table accordingly.
ID: sending-pipeline-infrastructure.esp-integration.webhook-delivery-status
Severity: high
What to look for: Check how the application learns about delivery status (delivered, bounced, complained, unsubscribed). Look for background jobs that call the ESP's message status API on a timer (polling). The correct pattern is to register a webhook endpoint that the ESP calls when a delivery event occurs. Look for a webhook handler route (/api/webhooks/email, /api/events/sendgrid, etc.) and verify it processes ESP events.
Pass criteria: A webhook endpoint exists that accepts delivery event payloads from the ESP. Webhook signatures are verified via HMAC validation. The handler processes at least 3 event types (delivered, bounced, complained) and updates the internal delivery status model. Count the number of delivery event types handled.
Fail criteria: Delivery status is determined by polling the ESP's status API on a timer. No webhook endpoint exists. Delivery events are never recorded in the application's database. Or webhook exists but no signature verification.
Skip (N/A) when: The application uses a one-way send model with no delivery tracking — confirmed by the absence of delivery status fields in the schema.
Detail on fail: "No webhook endpoint registered for ESP delivery events — delivery status never recorded in the database" or "Background job polls SendGrid message status API every 5 minutes — inefficient, delayed, and subject to rate limits"
Remediation: Register a webhook endpoint and handle ESP delivery events:
// app/api/webhooks/sendgrid/route.ts (Next.js App Router)
import crypto from 'crypto'
import { db } from '@/lib/db'
export async function POST(req: Request) {
// Verify SendGrid webhook signature
const signature = req.headers.get('x-twilio-email-event-webhook-signature') ?? ''
const timestamp = req.headers.get('x-twilio-email-event-webhook-timestamp') ?? ''
const body = await req.text()
const payload = timestamp + body
const computed = crypto
.createHmac('sha256', process.env.SENDGRID_WEBHOOK_SECRET!)
.update(payload)
.digest('base64')
if (computed !== signature) {
return new Response('Invalid signature', { status: 401 })
}
const events: Array<{ email: string; event: string; sg_message_id: string }> = JSON.parse(body)
for (const event of events) {
await db.emailSendLog.updateMany({
where: { espMessageId: event.sg_message_id },
data: { deliveryStatus: event.event, updatedAt: new Date() }
})
}
return new Response('OK', { status: 200 })
}