RavenRelayDocs
Dashboard ↗

Delivery

Signed webhooks

Receive delivery outcomes in your application with authenticated payloads, retries and delivery logs.

Create an endpoint

  1. Implement a public HTTPS POST endpoint in your application on port 443.
  2. In the project's Webhooks screen, add the endpoint and choose event subscriptions.
  3. Save the signing secret shown once in your receiving application's server environment.
  4. 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

Signed headers
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.

Node.js · no SDK installation required
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

Example delivery event
{  "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.

RavenRelay documentation. Examples describe the current implementation.