Command Palette

Search for a command to run...

Guide

Webhooks

Receive real-time notifications about email events like deliveries, opens, clicks, and bounces via HTTP webhooks.

How Webhooks Work

1

Event Occurs

Email is delivered, opened, clicked, etc.

2

We Send POST

HTTP POST to your endpoint

3

You Verify

Validate signature for security

4

Process Event

Update your database, trigger actions

Event Types

EventDescription
email.sentEmail was sent to recipient
email.deliveredEmail was delivered to recipient's inbox
email.openedRecipient opened the email
email.clickedRecipient clicked a link in the email
email.bouncedEmail bounced (hard or soft)
email.complainedRecipient marked as spam
contact.unsubscribedContact unsubscribed from emails

Creating a Webhook

Create a webhook endpoint to receive events:

cURL
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

response.json
{
  "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"
}

Important: Store the signing_secret securely - you'll need it to verify webhooks.

Webhook Payload

Each webhook request includes the following structure:

webhook-payload.json
{
  "id": "evt_xxxxxxxxxxxxxxxx",
  "type": "email.delivered",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "email_id": "eml_xxxxxxxxxxxxxxxx",
    "from": "hello@yourdomain.com",
    "to": "user@example.com",
    "subject": "Welcome to Our Platform",
    "tags": {
      "campaign": "onboarding"
    }
  }
}

Click Event Example

click-event.json
{
  "id": "evt_xxxxxxxxxxxxxxxx",
  "type": "email.clicked",
  "created_at": "2024-01-15T11:45:00Z",
  "data": {
    "email_id": "eml_xxxxxxxxxxxxxxxx",
    "to": "user@example.com",
    "link": "https://yourdomain.com/pricing",
    "user_agent": "Mozilla/5.0...",
    "ip_address": "192.168.1.1"
  }
}

Bounce Event Example

bounce-event.json
{
  "id": "evt_xxxxxxxxxxxxxxxx",
  "type": "email.bounced",
  "created_at": "2024-01-15T10:32:00Z",
  "data": {
    "email_id": "eml_xxxxxxxxxxxxxxxx",
    "to": "invalid@example.com",
    "bounce_type": "hard",
    "bounce_reason": "User unknown"
  }
}

Verifying Webhook Signatures

Important: Always verify webhook signatures to ensure requests are from Unosend.

Each webhook request includes a signature in the X-Unosend-Signature header:

verify-webhook.ts
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

Next.js App Router

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 });
}

Python Flask

webhooks.py
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = 'whsec_your_secret'

def verify_signature(payload: bytes, signature: str) -> bool:
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return signature == f'sha256={expected}'

@app.route('/api/webhooks/unosend', methods=['POST'])
def handle_webhook():
    payload = request.get_data()
    signature = request.headers.get('X-Unosend-Signature')
    
    if not signature or not verify_signature(payload, signature):
        return jsonify({'error': 'Invalid signature'}), 401
    
    event = request.get_json()
    
    if event['type'] == 'email.delivered':
        print(f"Email delivered: {event['data']['email_id']}")
    elif event['type'] == 'email.bounced':
        print(f"Email bounced: {event['data']['to']}")
    
    return jsonify({'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
curl https://www.unosend.co/api/v1/webhooks \
  -H "Authorization: Bearer un_your_api_key"

Update a Webhook

cURL
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
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