Webhooks
Receive real-time notifications for email, contact, and domain events via HTTP webhooks. PostStack sends POST requests to your endpoint when events occur. Includes delivery tracking and replay for failed deliveries.
/webhooksCreate a new webhook endpoint. Subscribe to specific events.
{
"url": "https://yourdomain.com/webhooks/poststack",
"events": [
"email.delivered",
"email.bounced",
"email.complained",
"email.opened",
"email.clicked"
]
}/webhooksList all webhook endpoints for your account.
{
"webhooks": [
{
"id": 1,
"url": "https://yourdomain.com/webhooks/poststack",
"events": ["email.delivered", "email.bounced"],
"active": true,
"created_at": "2026-03-23T10:00:00.000Z"
}
]
}/webhooks/:idRetrieve a single webhook endpoint with its configuration.
{
"webhook": {
"id": 1,
"url": "https://yourdomain.com/webhooks/poststack",
"events": ["email.delivered", "email.bounced", "email.opened"],
"active": true,
"created_at": "2026-03-23T10:00:00.000Z"
}
}/webhooks/:idUpdate a webhook's URL, events, or enabled status.
{
"url": "https://yourdomain.com/webhooks/v2",
"events": ["email.delivered", "email.bounced", "email.opened"],
"enabled": true
}/webhooks/:idDelete a webhook endpoint. No further events will be delivered.
{
"success": true
}/webhooks/:id/testSend a test event to your webhook endpoint to verify it is working correctly.
{
"success": true
}/webhooks/:id/deliveriesList recent webhook delivery attempts with status, response codes, and timestamps.
{
"deliveries": [
{
"id": 1,
"webhook_id": 1,
"event": "email.delivered",
"status_code": 200,
"success": true,
"attempts": 1,
"created_at": "2026-03-23T10:00:02.000Z",
"next_attempt_at": null
},
{
"id": 2,
"webhook_id": 1,
"event": "email.bounced",
"status_code": 500,
"success": false,
"attempts": 3,
"created_at": "2026-03-23T10:05:00.000Z",
"next_attempt_at": "2026-03-23T11:05:00.000Z"
}
],
"pagination": {
"page": 1,
"perPage": 20,
"total": 156,
"totalPages": 8
}
}/webhooks/:id/deliveries/:did/replayReplay a failed webhook delivery. Sends the original event payload to your endpoint again.
{
"success": true
}/webhooks/:id/rotate-secretRotate the webhook's signing secret. Returns a new plaintext secret (shown once). Takes effect immediately — update your endpoint's verification before the next event.
{
"webhook": {
"id": 1,
"url": "https://yourdomain.com/webhooks/v2",
"secret_rotated_at": "2026-03-23T15:00:00.000Z"
},
"signingSecret": "whsec_NEW..."
}Delivery & retries: any 2xx response counts as delivered. Non-2xx responses are retried with exponential backoff (up to 8 attempts). Respond with 406 Not Acceptable to reject an event without triggering retries — useful for events you intentionally don't want (deduped or irrelevant). Rejected events don't count toward the consecutive-failure limit that auto-disables an endpoint.
Webhook Events
Subscribe to any combination of the following events:
Email Events
| Event | Description |
|---|---|
email.sent | Email has been sent to the recipient mail server |
email.delivered | Email was successfully delivered |
email.bounced | Email hard bounced (permanent failure) |
email.soft_bounced | Email soft bounced (temporary failure) |
email.opened | Recipient opened the email (if tracking enabled) |
email.clicked | Recipient clicked a link (if tracking enabled) |
email.complained | Recipient marked the email as spam |
email.unsubscribed | Recipient clicked the unsubscribe link |
email.failed | Email delivery failed |
email.delivery_delayed | Email delivery was delayed |
email.scheduled | Email was scheduled for future delivery |
email.suppressed | Email was suppressed (recipient on suppression list) |
email.inbound | Inbound email received on your domain |
Contact Events
| Event | Description |
|---|---|
contact.created | A new contact was created |
contact.updated | A contact was updated |
contact.deleted | A contact was deleted |
contact.unsubscribed | A contact unsubscribed |
Domain Events
| Event | Description |
|---|---|
domain.created | A new domain was added |
domain.updated | Domain settings were updated |
domain.deleted | A domain was removed |
domain.verified | Domain DNS verification succeeded |
domain.failed | Domain DNS verification failed |
Webhook Payload
Each webhook delivery includes the event type, timestamp, and relevant data:
{
"type": "email.delivered",
"created_at": "2026-03-23T10:00:02.000Z",
"data": {
"email_id": "em_abc123def456ghi789",
"from": "you@yourdomain.com",
"to": "user@example.com",
"subject": "Welcome to PostStack"
}
}Bounce events (email.bounced and email.soft_bounced) add the receiving server's diagnostic, its RFC 3463 status code, and a stable bounce_category you can branch on — one of invalid_mailbox, mailbox_full, spam_block, message_too_large, auth_failure, rate_limited, dns_failure, connection_failed, content_rejected, policy, or unknown:
{
"type": "email.bounced",
"created_at": "2026-03-23T10:00:02.000Z",
"data": {
"email_id": "em_abc123def456ghi789",
"from": "you@yourdomain.com",
"to": "user@example.com",
"subject": "Welcome to PostStack",
"bounce_message": "550 5.1.1 <user@example.com>: Recipient address rejected: User unknown",
"bounce_code": "5.1.1",
"bounce_category": "invalid_mailbox"
}
}Signature Verification
Every webhook request includes an X-PostStack-Signature header. Verify it to ensure the request came from PostStack:
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string,
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const expectedSignature = `sha256=${expected}`;
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature),
);
}
// In your webhook handler:
app.post('/webhooks/poststack', (req, res) => {
const signature = req.headers['x-poststack-signature'];
const body = JSON.stringify(req.body);
if (!verifyWebhookSignature(body, signature, 'whsec_abc123...')) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the event
const event = req.body;
switch (event.type) {
case 'email.delivered':
// Handle delivery
break;
case 'email.bounced':
// Handle bounce
break;
}
res.status(200).json({ received: true });
});