Xurel Docs

Webhooks

Webhooks deliver delivery lifecycle events to your HTTPS endpoint. Always verify X-Xurel-Signature before trusting the payload.

Event types

EventWhen
email.queuedAccepted into the send queue
email.sentHanded off to the MTA
email.deliveredRecipient server accepted the message
email.bouncedHard or soft bounce
email.complainedSpam complaint
email.openedOpen tracked (if enabled)
email.clickedLink clicked (if enabled)
email.delivery_delayedTemporary deferral

Create endpoint

POST /v1/webhooks
Field Type Required Description
url string (https) Required HTTPS endpoint that receives events.
events string[] Required Event types to subscribe to.
description string Optional Human label for the endpoint.
201
{
  "id": "wh_01",
  "url": "https://example.com/hooks/xurel",
  "events": ["email.delivered", "email.bounced"],
  "secret": "whsec_xxxxxxxx",
  "status": "enabled"
}

Signature verification

Header format: X-Xurel-Signature: t=<unix>,v1=<hex>.

  1. Parse t and v1 from the header.
  2. Reject if timestamp skew > 5 minutes.
  3. Compute HMAC-SHA256(secret, "{t}." + rawBody) as hex.
  4. Compare with v1 using a constant-time equality check.
Event payload
{
  "type": "email.delivered",
  "createdAt": "2026-07-12T10:00:04Z",
  "data": {
    "emailId": "em_a1b2c3d4",
    "to": "user@example.com",
    "from": "hello@yourdomain.com"
  }
}

Send test event

POST /v1/webhooks/{id}/test

Posts a sample event to the endpoint so you can verify wiring without a real send.

List, update, delete

GET /v1/webhooks
GET /v1/webhooks/{id}
PATCH /v1/webhooks/{id}
DELETE /v1/webhooks/{id}

Verify signature

Node.js
import crypto from 'crypto';

export function verifyXurelSignature(rawBody, signatureHeader, secret) {
  // X-Xurel-Signature: t=<unix>,v1=<hex>
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.trim().split('='))
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;

  const skew = Math.abs(Date.now() / 1000 - Number(t));
  if (skew > 300) return false; // 5 minutes

  const signed = `${t}.${rawBody}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signed, 'utf8')
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(v1, 'hex')
  );
}
Python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        return False
    if abs(time.time() - int(t)) > 300:
        return False
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)