Webhooks
Webhooks API
Section titled “Webhooks API”Configure webhook endpoints to receive real-time email delivery events from Optail.
GET /v1/webhook-endpoints
Section titled “GET /v1/webhook-endpoints”List all webhook endpoints for the organization.
Response
Section titled “Response”{ "data": [ { "id": "wh-uuid", "organizationId": "org-uuid", "url": "https://yourapp.com/webhooks/optail", "events": ["DELIVERED", "BOUNCED", "COMPLAINED", "OPENED", "CLICKED"], "isActive": true, "createdAt": "2025-01-10T08:00:00Z", "updatedAt": "2025-01-10T08:00:00Z" } ]}POST /v1/webhook-endpoints
Section titled “POST /v1/webhook-endpoints”Create a new webhook endpoint. The URL is validated for reachability before creation. A signing secret is generated and returned once — store it securely.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL that will receive webhook POST requests. |
events | string[] | Yes | Event types to subscribe to (at least one). |
isActive | boolean | No | Whether the endpoint is active (default: true). |
Available Event Types
Section titled “Available Event Types”| Event | Description |
|---|---|
QUEUED | Email accepted and queued for delivery. |
SENT | Email handed off to the provider. |
DELIVERED | Provider confirmed delivery to recipient’s mailbox. |
OPENED | Recipient opened the email (tracking pixel). |
CLICKED | Recipient clicked a link in the email. |
BOUNCED | Email bounced (hard, soft, or block). |
COMPLAINED | Recipient marked the email as spam. |
UNSUBSCRIBED | Recipient unsubscribed via the unsubscribe link. |
DROPPED | Email was dropped before delivery (e.g., invalid address). |
DEFERRED | Delivery was temporarily deferred by the receiving server. |
Response
Section titled “Response”Status: 201 Created
{ "data": { "id": "wh-uuid", "organizationId": "org-uuid", "url": "https://yourapp.com/webhooks/optail", "events": ["DELIVERED", "BOUNCED", "COMPLAINED"], "isActive": true, "createdAt": "2025-01-10T08:00:00Z", "updatedAt": "2025-01-10T08:00:00Z", "secret": "whsec_a1b2c3d4e5f6..." }}PATCH /v1/webhook-endpoints/:id
Section titled “PATCH /v1/webhook-endpoints/:id”Update a webhook endpoint’s URL, events, or active status. If the URL changes, it is re-validated for reachability.
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
url | string | No | New webhook URL. |
events | string[] | No | New event subscriptions. |
isActive | boolean | No | Enable or disable the endpoint. |
DELETE /v1/webhook-endpoints/:id
Section titled “DELETE /v1/webhook-endpoints/:id”Delete a webhook endpoint.
POST /v1/webhook-endpoints/:id/test
Section titled “POST /v1/webhook-endpoints/:id/test”Send a test webhook event to verify your endpoint is receiving data correctly.
Response
Section titled “Response”{ "success": true, "statusCode": 200, "responseTime": "under 10s"}Webhook Payload Format
Section titled “Webhook Payload Format”When an event occurs, Optail sends a POST request to your endpoint with the following format:
{ "event": "DELIVERED", "timestamp": "2025-01-15T10:00:03Z", "data": { "messageId": "msg-uuid", "eventType": "DELIVERED", "recipient": "user@example.com", "from": "hello@yourdomain.com", "subject": "Welcome!" }}Request Headers
Section titled “Request Headers”| Header | Description |
|---|---|
Content-Type | application/json |
X-Optail-Signature | HMAC-SHA256 signature for payload verification. |
X-Optail-Timestamp | Unix timestamp (seconds) of when the webhook was sent. |
X-Optail-Event | Event type (e.g., DELIVERED). |
Signature Verification
Section titled “Signature Verification”Every webhook request includes a signature in the X-Optail-Signature header. You should verify this signature to confirm the request came from Optail and was not tampered with.
The signature is computed as:
HMAC-SHA256(secret, "${timestamp}.${payload}")Where timestamp is the X-Optail-Timestamp header value and payload is the raw JSON request body.
Using the Node.js SDK
Section titled “Using the Node.js SDK”import { verifyWebhookSignature } from 'optail';
app.post('/webhooks/optail', (req, res) => { const signature = req.headers['x-optail-signature']; const timestamp = req.headers['x-optail-timestamp']; const rawBody = req.body; // raw string, not parsed JSON
const isValid = verifyWebhookSignature( rawBody, signature, process.env.OPTAIL_WEBHOOK_SECRET, );
if (!isValid) { return res.status(401).send('Invalid signature'); }
const event = JSON.parse(rawBody); // Process the event...
res.status(200).send('OK');});Manual Verification
Section titled “Manual Verification”import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(payload: string, signature: string, secret: string): boolean { const expected = createHmac('sha256', secret) .update(payload) .digest('hex');
const expectedBuf = Buffer.from(expected, 'utf-8'); const signatureBuf = Buffer.from(signature, 'utf-8');
if (expectedBuf.length !== signatureBuf.length) { return false; }
return timingSafeEqual(expectedBuf, signatureBuf);}Best Practices
Section titled “Best Practices”- Always verify signatures before processing webhook events.
- Respond with 2xx quickly — process events asynchronously if your logic is slow.
- Handle duplicates — Optail may retry delivery, so your handler should be idempotent.
- Check the timestamp — reject events older than 5 minutes to prevent replay attacks.