unsent
unsent.dev
API Reference

Webhooks

The Webhooks API allows you to receive real-time notifications when events occur in your Unsent account.

Overview

Webhooks enable your application to receive real-time notifications when specific events occur in your Unsent account. Instead of continuously polling the API to check for changes, webhooks push event data to your server as events happen, making them ideal for building reactive, event-driven applications.

Base URL

https://api.unsent.dev/v1/webhooks

Features

Real-time notifications

Receive instant updates when events occur

Event filtering

Subscribe to specific event types you care about

Secure delivery

HMAC signature verification for secure webhooks

Automatic retries

Failed deliveries are automatically retried

Webhook testing

Test your webhook endpoints before going live

Monitoring

Track webhook delivery status and failures


Endpoints

Create Webhook

POST /v1/webhooks

Register a new webhook endpoint for your team

Request Body:

FieldTypeRequiredDescription
urlstring (URL)YesThe HTTPS endpoint to receive webhook events
eventTypesarrayYesArray of event types to subscribe to (see Event Types)
descriptionstringNoOptional description for this webhook
secretstringNoOptional secret key for HMAC signature verification (min 16 characters). If not provided, one will be generated automatically
curl -X POST https://api.unsent.dev/v1/webhooks \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourdomain.com/webhooks/unsent",
    "description": "Production webhook endpoint",
    "eventTypes": [
      "email.sent",
      "email.delivered",
      "email.bounced",
      "email.opened",
      "email.clicked"
    ],
    "secret": "your-secret-key-at-least-16-chars"
  }'

Response (200 OK):

{
  "id": "wh_abc123def456",
  "url": "https://yourdomain.com/webhooks/unsent",
  "description": "Production webhook endpoint",
  "eventTypes": [
    "email.sent",
    "email.delivered",
    "email.bounced",
    "email.opened",
    "email.clicked"
  ],
  "status": "ACTIVE",
  "secret": "your-secret-key-at-least-16-chars",
  "apiVersion": null,
  "consecutiveFailures": 0,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-01-15T10:30:00Z",
  "teamId": "team_xyz789",
  "createdByUserId": "user_abc123"
}

[!IMPORTANT] The secret field in the response is the only time you'll see the full secret value. Store it securely to verify webhook signatures.


List All Webhooks

GET /v1/webhooks

Retrieve all webhooks registered for your team

curl -X GET https://api.unsent.dev/v1/webhooks \
  -H "Authorization: Bearer your-api-key"

Response (200 OK):

[
  {
    "id": "wh_abc123def456",
    "url": "https://yourdomain.com/webhooks/unsent",
    "description": "Production webhook endpoint",
    "eventTypes": [
      "email.sent",
      "email.delivered",
      "email.bounced"
    ],
    "status": "ACTIVE",
    "secret": "wh_sec_***",
    "apiVersion": null,
    "consecutiveFailures": 0,
    "lastSuccessAt": "2024-01-15T12:00:00Z",
    "lastFailureAt": null,
    "createdAt": "2024-01-15T10:30:00Z",
    "updatedAt": "2024-01-15T10:30:00Z",
    "teamId": "team_xyz789",
    "createdByUserId": "user_abc123"
  }
]

[!NOTE] The secret field is masked in list responses for security purposes.


Get Webhook by ID

GET /v1/webhooks/{id}

Retrieve detailed information about a specific webhook

Path Parameters:

  • id (string, required) - The webhook ID
curl -X GET https://api.unsent.dev/v1/webhooks/wh_abc123def456 \
  -H "Authorization: Bearer your-api-key"

Response (200 OK):

{
  "id": "wh_abc123def456",
  "url": "https://yourdomain.com/webhooks/unsent",
  "description": "Production webhook endpoint",
  "eventTypes": [
    "email.sent",
    "email.delivered",
    "email.bounced"
  ],
  "status": "ACTIVE",
  "secret": "wh_sec_***",
  "apiVersion": null,
  "consecutiveFailures": 0,
  "lastSuccessAt": "2024-01-15T12:00:00Z",
  "lastFailureAt": null,
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-01-15T10:30:00Z",
  "teamId": "team_xyz789",
  "createdByUserId": "user_abc123"
}

Error Responses:

  • 404 Not Found - Webhook not found
    {
      "code": "NOT_FOUND",
      "message": "Webhook not found"
    }

Update Webhook

PATCH /v1/webhooks/{id}

Update an existing webhook configuration

Path Parameters:

  • id (string, required) - The webhook ID

Request Body:

FieldTypeRequiredDescription
urlstring (URL)NoNew webhook endpoint URL
descriptionstring | nullNoUpdated description
eventTypesarrayNoUpdated event types to subscribe to
activebooleanNoSet to false to pause the webhook, true to activate
rotateSecretbooleanNoSet to true to generate a new secret key
secretstringNoManually set a new secret (min 16 characters)
curl -X PATCH https://api.unsent.dev/v1/webhooks/wh_abc123def456 \
  -H "Authorization: Bearer your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "eventTypes": [
      "email.sent",
      "email.delivered",
      "email.bounced",
      "email.opened",
      "email.clicked",
      "email.complained"
    ],
    "description": "Updated production webhook"
  }'

Response (200 OK):

{
  "id": "wh_abc123def456",
  "url": "https://yourdomain.com/webhooks/unsent",
  "description": "Updated production webhook",
  "eventTypes": [
    "email.sent",
    "email.delivered",
    "email.bounced",
    "email.opened",
    "email.clicked",
    "email.complained"
  ],
  "status": "ACTIVE",
  "secret": "wh_sec_***",
  "apiVersion": null,
  "consecutiveFailures": 0,
  "lastSuccessAt": "2024-01-15T12:00:00Z",
  "lastFailureAt": null,
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-01-15T13:00:00Z",
  "teamId": "team_xyz789",
  "createdByUserId": "user_abc123"
}

[!WARNING] When using rotateSecret, the new secret will only be shown once in the response. Make sure to store it securely before the response is lost.

Error Responses:

  • 404 Not Found - Webhook not found
  • 400 Bad Request - Invalid request parameters

Delete Webhook

DELETE /v1/webhooks/{id}

Delete a webhook endpoint

Path Parameters:

  • id (string, required) - The webhook ID to delete
curl -X DELETE https://api.unsent.dev/v1/webhooks/wh_abc123def456 \
  -H "Authorization: Bearer your-api-key"

Response (200 OK):

{
  "id": "wh_abc123def456",
  "url": "https://yourdomain.com/webhooks/unsent",
  "description": "Production webhook endpoint",
  "eventTypes": [
    "email.sent",
    "email.delivered"
  ],
  "status": "ACTIVE",
  "secret": "wh_sec_***",
  "apiVersion": null,
  "consecutiveFailures": 0,
  "lastSuccessAt": "2024-01-15T12:00:00Z",
  "lastFailureAt": null,
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-01-15T13:00:00Z",
  "teamId": "team_xyz789",
  "createdByUserId": "user_abc123"
}

[!CAUTION] Deleting a webhook is permanent and cannot be undone. Any pending deliveries will be cancelled.

Error Responses:

  • 404 Not Found - Webhook not found

Test Webhook

POST /v1/webhooks/{id}/test

Trigger a test event to verify your webhook endpoint is working correctly

Path Parameters:

  • id (string, required) - The webhook ID to test
curl -X POST https://api.unsent.dev/v1/webhooks/wh_abc123def456/test \
  -H "Authorization: Bearer your-api-key"

Response (200 OK):

{
  "id": "whc_test123",
  "type": "webhook.test",
  "createdAt": "2024-01-15T14:00:00Z",
  "updatedAt": "2024-01-15T14:00:01Z",
  "teamId": "team_xyz789",
  "status": "SUCCESS",
  "webhookId": "wh_abc123def456",
  "payload": "{\"id\":\"evt_test123\",\"type\":\"webhook.test\",\"createdAt\":\"2024-01-15T14:00:00Z\",\"data\":{\"message\":\"This is a test webhook event\"}}",
  "attempt": 1,
  "nextAttemptAt": null,
  "lastError": null,
  "responseStatus": 200,
  "responseTimeMs": 145,
  "responseText": "{\"received\":true}"
}

[!NOTE] The test event sends a webhook.test event type that won't be included in your regular event subscriptions. Use this to verify your endpoint can receive and process webhook payloads.

Error Responses:

  • 500 Internal Server Error - Webhook delivery failed
    {
      "code": "INTERNAL_SERVER_ERROR",
      "message": "Failed to deliver test webhook"
    }

Event Types

Unsent supports the following webhook event types. You can subscribe to any combination of these events when creating or updating a webhook.

Email Events

Event TypeDescription
email.queuedEmail has been queued for sending
email.sentEmail has been successfully sent to the mail server
email.delivery_delayedEmail delivery is delayed (temporary issue)
email.deliveredEmail was successfully delivered to recipient's inbox
email.bouncedEmail bounced (permanent or temporary)
email.rejectedEmail was rejected by the recipient's server
email.rendering_failureEmail template failed to render
email.complainedRecipient marked the email as spam
email.failedEmail sending failed
email.cancelledEmail was cancelled before delivery
email.suppressedEmail was suppressed due to suppression list
email.openedRecipient opened the email (requires open tracking)
email.clickedRecipient clicked a link in the email (requires click tracking)

Contact Events

Event TypeDescription
contact.createdA new contact was created
contact.updatedAn existing contact was updated
contact.deletedA contact was deleted

Domain Events

Event TypeDescription
domain.createdA new domain was added
domain.verifiedA domain was successfully verified
domain.updatedDomain settings were updated
domain.deletedA domain was removed

Webhook Payload Structure

All webhook events follow a consistent payload structure:

{
  "id": "evt_unique_event_id",
  "type": "email.delivered",
  "createdAt": "2024-01-15T10:30:00Z",
  "data": {
    // Event-specific payload data
  }
}

Email Event Payload

{
  "id": "evt_abc123",
  "type": "email.delivered",
  "createdAt": "2024-01-15T10:30:00Z",
  "data": {
    "id": "email_xyz789",
    "status": "DELIVERED",
    "from": "noreply@yourdomain.com",
    "to": ["recipient@example.com"],
    "occurredAt": "2024-01-15T10:30:00Z",
    "subject": "Welcome to our service",
    "campaignId": "camp_123",
    "contactId": "contact_456",
    "domainId": 1,
    "templateId": "tpl_789",
    "metadata": {
      "userId": "user_abc",
      "environment": "production"
    }
  }
}

Email Bounced Event

{
  "id": "evt_bounce123",
  "type": "email.bounced",
  "createdAt": "2024-01-15T10:35:00Z",
  "data": {
    "id": "email_xyz789",
    "status": "BOUNCED",
    "from": "noreply@yourdomain.com",
    "to": ["invalid@example.com"],
    "occurredAt": "2024-01-15T10:35:00Z",
    "subject": "Welcome email",
    "bounce": {
      "type": "Permanent",
      "subType": "General",
      "message": "Mailbox does not exist"
    }
  }
}

Email Opened Event

{
  "id": "evt_open123",
  "type": "email.opened",
  "createdAt": "2024-01-15T11:00:00Z",
  "data": {
    "id": "email_xyz789",
    "status": "OPENED",
    "from": "noreply@yourdomain.com",
    "to": ["recipient@example.com"],
    "occurredAt": "2024-01-15T11:00:00Z",
    "subject": "Welcome email",
    "open": {
      "timestamp": "2024-01-15T11:00:00Z",
      "userAgent": "Mozilla/5.0...",
      "ip": "192.168.1.1",
      "platform": "Desktop"
    }
  }
}

Email Clicked Event

{
  "id": "evt_click123",
  "type": "email.clicked",
  "createdAt": "2024-01-15T11:05:00Z",
  "data": {
    "id": "email_xyz789",
    "status": "CLICKED",
    "from": "noreply@yourdomain.com",
    "to": ["recipient@example.com"],
    "occurredAt": "2024-01-15T11:05:00Z",
    "subject": "Welcome email",
    "click": {
      "timestamp": "2024-01-15T11:05:00Z",
      "url": "https://yourdomain.com/verify",
      "userAgent": "Mozilla/5.0...",
      "ip": "192.168.1.1",
      "platform": "Desktop"
    }
  }
}

Contact Event Payload

{
  "id": "evt_contact123",
  "type": "contact.created",
  "createdAt": "2024-01-15T10:00:00Z",
  "data": {
    "id": "contact_abc123",
    "email": "newuser@example.com",
    "contactBookId": "book_xyz789",
    "subscribed": true,
    "firstName": "John",
    "lastName": "Doe",
    "properties": {
      "plan": "premium",
      "signupDate": "2024-01-15"
    },
    "createdAt": "2024-01-15T10:00:00Z",
    "updatedAt": "2024-01-15T10:00:00Z"
  }
}

Domain Event Payload

{
  "id": "evt_domain123",
  "type": "domain.verified",
  "createdAt": "2024-01-15T09:00:00Z",
  "data": {
    "id": 1,
    "name": "yourdomain.com",
    "status": "SUCCESS",
    "region": "us-east-1",
    "clickTracking": true,
    "openTracking": true,
    "dkimStatus": "Success",
    "spfDetails": "v=spf1 include:amazonses.com ~all",
    "dmarcAdded": true,
    "createdAt": "2024-01-14T10:00:00Z",
    "updatedAt": "2024-01-15T09:00:00Z"
  }
}

Webhook Security

HMAC Signature Verification

Unsent signs all webhook requests using HMAC-SHA256 with your webhook secret. This allows you to verify that webhooks are genuinely from Unsent and haven't been tampered with.

Signature Header:

X-Unsent-Signature: sha256=<signature>

Verifying Signatures

Extract the signature from headers

Get the X-Unsent-Signature header value from the incoming webhook request.

const signature = request.headers['x-unsent-signature'];

Compute the expected signature

Use your webhook secret to compute an HMAC-SHA256 signature of the raw request body.

const crypto = require('crypto');

const expectedSignature = crypto
  .createHmac('sha256', webhookSecret)
  .update(rawRequestBody)
  .digest('hex');

const expectedHeader = `sha256=${expectedSignature}`;

Compare signatures securely

Use a constant-time comparison to prevent timing attacks.

const crypto = require('crypto');

function verifySignature(signature, expectedSignature) {
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

if (verifySignature(signature, expectedHeader)) {
  // Signature is valid, process the webhook
} else {
  // Invalid signature, reject the request
  return res.status(401).send('Invalid signature');
}

Complete Example (Node.js/Express)

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = 'your-webhook-secret';

app.post('/webhooks/unsent', 
  express.raw({ type: 'application/json' }), 
  (req, res) => {
    const signature = req.headers['x-unsent-signature'];
    
    // Compute expected signature
    const expectedSignature = crypto
      .createHmac('sha256', WEBHOOK_SECRET)
      .update(req.body)
      .digest('hex');
    
    const expectedHeader = `sha256=${expectedSignature}`;
    
    // Verify signature
    if (!crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedHeader)
    )) {
      return res.status(401).send('Invalid signature');
    }
    
    // Parse and process the webhook
    const event = JSON.parse(req.body.toString());
    
    console.log('Received event:', event.type);
    console.log('Event data:', event.data);
    
    // Process based on event type
    switch (event.type) {
      case 'email.delivered':
        // Handle delivered email
        break;
      case 'email.bounced':
        // Handle bounced email
        break;
      // ... other event types
    }
    
    res.status(200).send('Webhook received');
  }
);

[!IMPORTANT] Always verify webhook signatures before processing the payload to ensure the request is legitimate.


Webhook Status

Webhooks can have the following status values:

StatusDescription
ACTIVEWebhook is active and receiving events
PAUSEDWebhook is paused and not receiving events
FAILEDWebhook has been automatically paused due to repeated failures

Automatic Failure Management

  • If a webhook endpoint fails to respond with a 2xx status code, Unsent will retry the delivery with exponential backoff
  • After consecutive failures, the webhook may be automatically paused to prevent further failed attempts
  • You can monitor failures via the consecutiveFailures, lastSuccessAt, and lastFailureAt fields

Best Practices

  1. Use HTTPS endpoints only
    Webhooks must use HTTPS URLs for security. HTTP endpoints are not supported.

  2. Verify signatures
    Always validate the X-Unsent-Signature header to ensure webhooks are from Unsent.

  3. Respond quickly
    Process webhooks asynchronously. Respond with a 2xx status code within a few seconds to avoid timeouts.

  4. Handle idempotency
    Webhooks may be delivered more than once. Use the event id to deduplicate events.

  5. Monitor failures
    Regularly check webhook status and failure counts. Set up alerts for repeated failures.

  6. Use the test endpoint
    Test your webhook endpoint using POST /v1/webhooks/{id}/test before going live.

  7. Subscribe selectively
    Only subscribe to events you need to reduce unnecessary traffic.

  8. Store secrets securely
    Never expose webhook secrets in client-side code or version control.

  9. Implement retry logic
    If your processing fails, implement your own retry mechanism.

  10. Use appropriate event types
    Choose event types that match your use case (e.g., only subscribe to email.delivered instead of all email events if you only care about deliveries).


Common Issues & Troubleshooting

Webhook not receiving events

Cause: Webhook may be paused, endpoint unreachable, or event types not matching

Solution:

  • Verify webhook status is ACTIVE
  • Ensure your endpoint is publicly accessible via HTTPS
  • Check that you're subscribed to the event types you expect
  • Use the test endpoint to verify connectivity

Signature verification failing

Cause: Incorrect secret or request body modification

Solution:

  • Ensure you're using the correct webhook secret
  • Verify you're computing the signature on the raw request body (before JSON parsing)
  • Check that no middleware is modifying the request body

High consecutive failures

Cause: Endpoint errors, timeouts, or incorrect response codes

Solution:

  • Check your endpoint logs for errors
  • Ensure your endpoint responds within a few seconds
  • Return a 2xx status code to acknowledge receipt
  • Process webhooks asynchronously to avoid timeouts

Events delivered multiple times

Cause: Normal behavior - webhooks may be retried on network issues

Solution:

  • Implement idempotency using the event id field
  • Track processed event IDs to avoid duplicate processing

Security Considerations

[!CAUTION]

  • Always use HTTPS endpoints for webhook URLs
  • Verify webhook signatures on every request
  • Never expose your webhook secret in client code
  • Implement rate limiting on your webhook endpoint
  • Log webhook failures and monitor for suspicious activity
  • Rotate webhook secrets periodically for extra security

OpenAPI Specification