Webhooks API
Configure webhook endpoints to receive real-time email delivery events from Optail.
GET /v1/webhook-endpoints
List all webhook endpoints for the organization.
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
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
| 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
| 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
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..."
}
}
The secret is only returned on creation. Store it immediately -- it cannot be retrieved later.
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
| 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
Delete a webhook endpoint.
POST /v1/webhook-endpoints/:id/test
Send a test webhook event to verify your endpoint is receiving data correctly.
Response
{
"success": true,
"statusCode": 200,
"responseTime": "under 10s"
}
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
| 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
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
import { verifyWebhookSignature } from '@optail/node';
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
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
- 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.