Skip to main content

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

FieldTypeRequiredDescription
urlstringYesHTTPS URL that will receive webhook POST requests.
eventsstring[]YesEvent types to subscribe to (at least one).
isActivebooleanNoWhether the endpoint is active (default: true).

Available Event Types

EventDescription
QUEUEDEmail accepted and queued for delivery.
SENTEmail handed off to the provider.
DELIVEREDProvider confirmed delivery to recipient's mailbox.
OPENEDRecipient opened the email (tracking pixel).
CLICKEDRecipient clicked a link in the email.
BOUNCEDEmail bounced (hard, soft, or block).
COMPLAINEDRecipient marked the email as spam.
UNSUBSCRIBEDRecipient unsubscribed via the unsubscribe link.
DROPPEDEmail was dropped before delivery (e.g., invalid address).
DEFERREDDelivery 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..."
}
}
caution

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

FieldTypeRequiredDescription
urlstringNoNew webhook URL.
eventsstring[]NoNew event subscriptions.
isActivebooleanNoEnable 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

HeaderDescription
Content-Typeapplication/json
X-Optail-SignatureHMAC-SHA256 signature for payload verification.
X-Optail-TimestampUnix timestamp (seconds) of when the webhook was sent.
X-Optail-EventEvent 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.