> ## Documentation Index
> Fetch the complete documentation index at: https://docs.unosend.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

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

## How Webhooks Work

<Steps>
  <Step title="Event Occurs">
    Email is delivered, opened, clicked, etc.
  </Step>

  <Step title="We Send POST">
    HTTP POST to your endpoint
  </Step>

  <Step title="You Verify">
    Validate signature for security
  </Step>

  <Step title="Process Event">
    Update your database, trigger actions
  </Step>
</Steps>

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

## Creating a Webhook

Create a webhook endpoint to receive events:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.unosend.co/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"
      ]
    }'
  ```

  ```javascript JavaScript theme={null}
  await fetch('https://api.unosend.co/webhooks', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer un_your_api_key',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url: 'https://yourapp.com/api/webhooks/unosend',
      events: ['email.delivered', 'email.opened', 'email.clicked', 'email.bounced', 'email.complained']
    })
  });
  ```

  ```python Python theme={null}
  import requests

  requests.post('https://api.unosend.co/webhooks',
    headers={'Authorization': 'Bearer un_your_api_key'},
    json={
      'url': 'https://yourapp.com/api/webhooks/unosend',
      'events': ['email.delivered', 'email.opened', 'email.clicked', 'email.bounced', 'email.complained']
    }
  )
  ```
</CodeGroup>

### Response

```json response.json theme={null}
{
  "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"
}
```

<Warning>
  Store the `signing_secret` securely - you'll need it to verify webhooks.
</Warning>

## Webhook Payload

Each webhook request includes the following structure:

```json webhook-payload.json theme={null}
{
  "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

```json click-event.json theme={null}
{
  "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

```json bounce-event.json theme={null}
{
  "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

<Warning>
  Always verify webhook signatures to ensure requests are from Unosend.
</Warning>

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

```typescript verify-webhook.ts theme={null}
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

<CodeGroup>
  ```typescript Next.js App Router theme={null}
  // 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 Python Flask theme={null}
  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})
  ```
</CodeGroup>

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

```bash cURL theme={null}
curl https://api.unosend.co/webhooks \
  -H "Authorization: Bearer un_your_api_key"
```

### Update a Webhook

```bash cURL theme={null}
curl -X PATCH https://api.unosend.co/webhooks/whk_xxxxxxxx \
  -H "Authorization: Bearer un_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["email.delivered", "email.bounced"]
  }'
```

### Delete a Webhook

```bash cURL theme={null}
curl -X DELETE https://api.unosend.co/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
