Delivery
Signed webhooks
Receive delivery outcomes in your application with authenticated payloads, retries and delivery logs.
Create an endpoint
- Implement a public HTTPS POST endpoint in your application on port 443.
- In the project's Webhooks screen, add the endpoint and choose event subscriptions.
- Save the signing secret shown once in your receiving application's server environment.
- Create the endpoint before sending a controlled live email, then inspect delivery logs and attempts.
Supported events: email.sent, email.delivered, email.bounced, email.complained, email.rejected, email.failed, email.delayed and email.suppressed. Test-mode emails do not call external endpoints. Events from before endpoint creation are not replayed.
Verify the raw body
RavenRelay-Event-Id: <stable event ID>RavenRelay-Timestamp: <Unix seconds>RavenRelay-Signature: v1=<hex HMAC-SHA256>Compute HMAC-SHA256 with the endpoint secret as the literal UTF-8 key and timestamp + '.' + raw_body as the input. Compare in constant time and reject timestamps older than five minutes. Verify the original bytes before parsing JSON.
import { createHmac, timingSafeEqual } from 'node:crypto';export async function POST(request: Request) { const body = await request.text(); const timestamp = request.headers.get('RavenRelay-Timestamp') ?? ''; const signature = request.headers.get('RavenRelay-Signature') ?? ''; const secret = process.env.RAVENRELAY_WEBHOOK_SECRET; if (!secret) return new Response(null, { status: 503 }); if (!/^\d{1,12}$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300 || !/^v1=[a-f0-9]{64}$/.test(signature)) { return new Response(null, { status: 401 }); } const expected = createHmac('sha256', secret) .update(timestamp + '.').update(body).digest(); const received = Buffer.from(signature.slice(3), 'hex'); if (!timingSafeEqual(expected, received)) { return new Response(null, { status: 401 }); } const event = JSON.parse(body); // Persist/enqueue once, using event.id as a unique key. // Return 2xx only after durable acceptance succeeds. return new Response(null, { status: 204 });}Event payload
{ "id": "your-event-id", "type": "email.delivered", "created_at": "2026-10-07T12:00:00.000Z", "data": { "email_id": "your-email-id", "project_id": "your-project-id", "status": "delivered", "recipients": ["user@example.net"], "provider_message_id": "provider-message-id" }}Check affected recipients rather than assuming every recipient succeeded. BCC addresses, message bodies and raw SMTP diagnostics are omitted. A bounce event uses data.status to distinguish hard_bounced from soft_bounced.
Retries and delivery logs
Only 2xx acknowledges success. Other statuses, timeouts and network errors retry after 1 minute, 5 minutes, 15 minutes, 1 hour, 6 hours and 24 hours, for seven attempts total. Unsafe destinations and invalid secrets stop immediately. Redirects are never followed.
Retries preserve the event ID and identical payload bytes, with a fresh timestamp and signature. Deduplicate by event ID and tolerate out-of-order delivery. Inspect Attempts in the dashboard for HTTP status, sanitized errors and the next retry. Manual replay is not implemented.
Rotate or pause
Rotate a signing secret in the dashboard and update your receiver immediately. An in-flight attempt may still use the old secret. Pausing or deleting an endpoint stops pending deliveries when processed; it cannot recall an in-flight request. Resuming does not replay cancelled deliveries.