Webhooks
Webhooks deliver delivery lifecycle events to your HTTPS endpoint.
Always verify X-Xurel-Signature before trusting the payload.
Event types
| Event | When |
|---|---|
email.queued | Accepted into the send queue |
email.sent | Handed off to the MTA |
email.delivered | Recipient server accepted the message |
email.bounced | Hard or soft bounce |
email.complained | Spam complaint |
email.opened | Open tracked (if enabled) |
email.clicked | Link clicked (if enabled) |
email.delivery_delayed | Temporary 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>.
- Parse
tandv1from the header. - Reject if timestamp skew > 5 minutes.
- Compute
HMAC-SHA256(secret, "{t}." + rawBody)as hex. - Compare with
v1using 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)