How Webhooks Work
Event Occurs
Email is delivered, opened, clicked, etc.
We Send POST
HTTP POST to your endpoint
You Verify
Validate signature for security
Process Event
Update your database, trigger actions
Event Types
| Event | Description |
|---|
email.sent | Email was sent to recipient |
email.delivered | Email was delivered to recipient’s inbox |
email.opened | Recipient opened the email |
email.clicked | Recipient clicked a link in the email |
email.bounced | Email bounced (hard or soft) |
email.complained | Recipient marked as spam |
contact.unsubscribed | Contact unsubscribed from emails |
Creating a Webhook
Create a webhook endpoint to receive events:
curl -X POST https://www.unosend.co/api/v1/webhooks \
-H "Authorization: Bearer un_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/api/webhooks/unosend",
"events": [
"email.delivered",
"email.opened",
"email.clicked",
"email.bounced",
"email.complained"
]
}'
Response
{
"id": "whk_xxxxxxxxxxxxxxxx",
"url": "https://yourapp.com/api/webhooks/unosend",
"events": [
"email.delivered",
"email.opened",
"email.clicked",
"email.bounced",
"email.complained"
],
"signing_secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxx",
"created_at": "2024-01-15T10:30:00Z"
}
Store the signing_secret securely - you’ll need it to verify webhooks.
Webhook Payload
Each webhook request includes the following structure:
{
"id": "evt_xxxxxxxxxxxxxxxx",
"type": "email.delivered",
"created_at": "2024-01-15T10:30:00Z",
"data": {
"email_id": "eml_xxxxxxxxxxxxxxxx",
"from": "[email protected]",
"to": "[email protected]",
"subject": "Welcome to Our Platform",
"tags": {
"campaign": "onboarding"
}
}
}
Click Event Example
{
"id": "evt_xxxxxxxxxxxxxxxx",
"type": "email.clicked",
"created_at": "2024-01-15T11:45:00Z",
"data": {
"email_id": "eml_xxxxxxxxxxxxxxxx",
"to": "[email protected]",
"link": "https://yourdomain.com/pricing",
"user_agent": "Mozilla/5.0...",
"ip_address": "192.168.1.1"
}
}
Bounce Event Example
{
"id": "evt_xxxxxxxxxxxxxxxx",
"type": "email.bounced",
"created_at": "2024-01-15T10:32:00Z",
"data": {
"email_id": "eml_xxxxxxxxxxxxxxxx",
"to": "[email protected]",
"bounce_type": "hard",
"bounce_reason": "User unknown"
}
}
Verifying Webhook Signatures
Always verify webhook signatures to ensure requests are from Unosend.
Each webhook request includes a signature in the X-Unosend-Signature header:
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expectedSignature}`)
);
}
Complete Webhook Handler
// app/api/webhooks/unosend/route.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';
function verifySignature(payload: string, signature: string): boolean {
const secret = process.env.UNOSEND_WEBHOOK_SECRET!;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return signature === `sha256=${expected}`;
}
export async function POST(request: Request) {
const payload = await request.text();
const signature = request.headers.get('X-Unosend-Signature');
if (!signature || !verifySignature(payload, signature)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
const event = JSON.parse(payload);
switch (event.type) {
case 'email.delivered':
console.log('Email delivered:', event.data.email_id);
// Update email status in database
break;
case 'email.opened':
console.log('Email opened:', event.data.email_id);
// Track email open
break;
case 'email.clicked':
console.log('Link clicked:', event.data.link);
// Track link click
break;
case 'email.bounced':
console.log('Email bounced:', event.data.to);
// Mark contact as bounced
break;
case 'email.complained':
console.log('Spam complaint:', event.data.to);
// Unsubscribe user
break;
default:
console.log('Unhandled event type:', event.type);
}
return NextResponse.json({ received: true });
}
Retry Logic
If your endpoint returns a non-2xx status code, we’ll retry the webhook:
- 1st retry: 1 minute after failure
- 2nd retry: 5 minutes after failure
- 3rd retry: 30 minutes after failure
- 4th retry: 2 hours after failure
- 5th retry: 8 hours after failure
After 5 failed attempts, the webhook is marked as failed and won’t be retried.
Managing Webhooks
List Webhooks
curl https://www.unosend.co/api/v1/webhooks \
-H "Authorization: Bearer un_your_api_key"
Update a Webhook
curl -X PATCH https://www.unosend.co/api/v1/webhooks/whk_xxxxxxxx \
-H "Authorization: Bearer un_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"events": ["email.delivered", "email.bounced"]
}'
Delete a Webhook
curl -X DELETE https://www.unosend.co/api/v1/webhooks/whk_xxxxxxxx \
-H "Authorization: Bearer un_your_api_key"
Best Practices
- ✓ Always verify signatures to ensure webhooks are from Unosend
- ✓ Return 200 quickly and process events asynchronously
- ✓ Handle duplicates - use event ID for idempotency
- ✓ Log all events for debugging and auditing
- ✓ Use HTTPS for your webhook endpoint