Skip to main content

Overview

Webhooks allow you to receive HTTP POST requests to your server when events occur in useSend, such as when an email is delivered, bounced, or clicked. This enables you to build real-time integrations and automate workflows.

Setting up webhooks

1

Create a webhook endpoint

Create an endpoint on your server that can receive POST requests. The endpoint must:
  • Accept POST requests with JSON body
  • Return a 2xx status code to acknowledge receipt
  • Respond within 10 seconds
2

Add webhook in dashboard

Go to Webhooks in your useSend dashboard and create a new webhook:
  • Enter your endpoint URL
  • Select which events you want to receive
  • Copy the signing secret for verification
3

Verify webhook signatures

Always verify webhook signatures to ensure requests are from useSend. See the Signature Verification section below.

Event types

Email events

EventDescription
email.queuedEmail has been queued for sending
email.sentEmail has been sent to the recipient’s mail server
email.deliveredEmail was successfully delivered
email.delivery_delayedEmail delivery is being retried
email.bouncedEmail bounced (permanent or temporary)
email.rejectedEmail was rejected
email.complainedRecipient marked email as spam
email.failedEmail failed to send
email.cancelledScheduled email was cancelled
email.suppressedEmail was suppressed (recipient on suppression list)
email.openedRecipient opened the email
email.clickedRecipient clicked a link in the email

Contact events

EventDescription
contact.createdNew contact was created
contact.updatedContact was updated
contact.deletedContact was deleted

Domain events

EventDescription
domain.createdNew domain was added
domain.verifiedDomain verification completed
domain.updatedDomain settings were updated
domain.deletedDomain was deleted

Webhook payload

Each webhook request includes a JSON payload with the following structure. See Event data details for details on the data field for each event type.
{
  "id": "call_abc123",
  "type": "email.delivered",
  "version": "2026-01-18",
  "createdAt": "2024-01-15T10:30:00.000Z",
  "teamId": 123,
  "data": {
    "id": "email_123",
    "status": "DELIVERED",
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome!",
    "occurredAt": "2024-01-15T10:30:00Z"
  },
  "attempt": 1
}

Payload fields

FieldDescription
idUnique identifier for this webhook call
typeThe event type (e.g., email.delivered)
versionAPI version for the payload format
createdAtWhen the event was created
teamIdYour team ID
dataEvent-specific data (varies by event type)
attemptDelivery attempt number (1-6)

Request headers

Each webhook request includes the following headers:
HeaderDescription
X-UseSend-SignatureHMAC-SHA256 signature for verification
X-UseSend-TimestampUnix timestamp in milliseconds
X-UseSend-EventEvent type
X-UseSend-CallUnique webhook call ID
X-UseSend-Retrytrue if this is a retry attempt

Signature verification

Always verify webhook signatures to ensure requests are authentic. The signature is computed as:
HMAC-SHA256(secret, "${timestamp}.${rawBody}")
npm install usesend

Next.js App Router

import { UseSend } from "usesend";

const usesend = new UseSend("us_your_api_key");
const webhooks = usesend.webhooks(process.env.USESEND_WEBHOOK_SECRET!);

export async function POST(request: Request) {
  try {
    const rawBody = await request.text();
    const event = webhooks.constructEvent(rawBody, {
      headers: request.headers,
    });

    switch (event.type) {
      case "email.delivered":
        console.log("Email delivered to:", event.data.to);
        break;
      case "email.bounced":
        console.log("Email bounced:", event.data.id);
        break;
      case "email.opened":
        console.log("Email opened:", event.data.id);
        break;
    }

    return new Response("ok");
  } catch (error) {
    console.error("Webhook error:", error);
    return new Response((error as Error).message, { status: 400 });
  }
}

Express

import express from "express";
import { Webhooks } from "usesend";

const webhooks = new Webhooks(process.env.USESEND_WEBHOOK_SECRET!);

const app = express();

// Important: Use raw body parser for webhook routes
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  try {
    const event = webhooks.constructEvent(req.body, {
      headers: req.headers,
    });

    switch (event.type) {
      case "email.delivered":
        console.log("Email delivered to:", event.data.to);
        break;
      case "email.bounced":
        console.log("Email bounced:", event.data.id);
        break;
    }

    res.status(200).send("ok");
  } catch (error) {
    console.error("Webhook error:", error);
    res.status(400).send((error as Error).message);
  }
});

app.listen(3000);

Verification only

If you only need to verify the signature without parsing:
const isValid = webhooks.verify(rawBody, { headers: request.headers });

if (!isValid) {
  return new Response("Invalid signature", { status: 401 });
}

Manual verification

If you prefer to verify manually without the SDK:
import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(
  secret: string,
  rawBody: string,
  signature: string,
  timestamp: string,
): boolean {
  const expectedSignature = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const expected = Buffer.from(`v1=${expectedSignature}`, "utf8");
  const received = Buffer.from(signature, "utf8");

  if (expected.length !== received.length) {
    return false;
  }

  return timingSafeEqual(expected, received);
}

// Usage
const signature = request.headers.get("X-UseSend-Signature");
const timestamp = request.headers.get("X-UseSend-Timestamp");

const isValid = verifyWebhook(secret, rawBody, signature, timestamp);

Retry behavior

If your endpoint doesn’t return a 2xx response, useSend will retry delivery with exponential backoff:
AttemptDelay
1Immediate
2~5 seconds
3~10 seconds
4~20 seconds
5~40 seconds
6~80 seconds
After 6 failed attempts, the webhook call is marked as failed.
If your webhook endpoint fails 30 consecutive times across any calls, the webhook will be automatically disabled to prevent continued failures. You can re-enable it from the dashboard.

Best practices

Return a 2xx response as soon as possible. Process webhook data asynchronously if needed. Requests timeout after 10 seconds.
Use the id field in the payload to deduplicate events. In rare cases, the same event may be delivered more than once.
Always verify the X-UseSend-Signature header to ensure requests are from useSend and haven’t been tampered with.
The SDK rejects signatures older than 5 minutes by default. This prevents replay attacks.
Always use HTTPS endpoints in production to encrypt webhook data in transit.

Testing webhooks

You can send a test webhook from the dashboard to verify your endpoint is working correctly:
  1. Go to Webhooks
  2. Click on your webhook
  3. Click “Send Test” to send a test event
The test event will have type webhook.test with the following payload:
{
  "test": true,
  "webhookId": "wh_abc123",
  "sentAt": "2024-01-15T10:30:00.000Z"
}

Troubleshooting

  • Verify your endpoint URL is correct and publicly accessible - Check that your endpoint returns a 2xx status code - Ensure the webhook is set to ACTIVE status in the dashboard - Check if the webhook was auto-disabled due to consecutive failures
  • Use the raw request body, not parsed JSON - Ensure you’re using the correct webhook secret - Check that the timestamp hasn’t expired (5 minute window) - Verify you’re computing the HMAC correctly: HMAC-SHA256(secret, "${timestamp}.${rawBody}")
After 30 consecutive failures, webhooks are automatically disabled. Fix the issue with your endpoint, then re-enable the webhook from the dashboard. The failure counter resets on the next successful delivery.

Event data details

This section documents the data field structure for each event type.

Email events

Most email events share a common base structure:
{
  id: string;              // Email ID
  status: string;          // Email status (e.g., "DELIVERED", "BOUNCED")
  from: string;            // Sender email address
  to: string[];            // Recipient email addresses
  occurredAt: string;      // ISO 8601 timestamp
  subject?: string;        // Email subject
  campaignId?: string;     // Campaign ID (if from a campaign)
  contactId?: string;      // Contact ID (if sent to a contact)
  domainId?: number;       // Domain ID
  templateId?: string;     // Template ID (if using a template)
  metadata?: object;       // Custom metadata you attached to the email
}

email.bounced

Includes additional bounce details:
{
  // ... base email fields
  bounce: {
    type: "Transient" | "Permanent" | "Undetermined";
    subType: "General" | "NoEmail" | "Suppressed" | "OnAccountSuppressionList"
           | "MailboxFull" | "MessageTooLarge" | "ContentRejected" | "AttachmentRejected";
    message?: string;      // Bounce message from the mail server
  }
}

email.failed

Includes failure reason:
{
  // ... base email fields
  failed: {
    reason: string; // Failure reason
  }
}

email.suppressed

Includes suppression details:
{
  // ... base email fields
  suppression: {
    type: "Bounce" | "Complaint" | "Manual";
    reason: string;        // Why the email was suppressed
    source?: string;       // Source of the suppression
  }
}

email.opened

Includes open tracking details:
{
  // ... base email fields
  open: {
    timestamp: string;     // When the email was opened
    userAgent?: string;    // Browser/client user agent
    ip?: string;           // IP address
    platform?: string;     // Detected platform
  }
}

email.clicked

Includes click tracking details:
{
  // ... base email fields
  click: {
    timestamp: string;     // When the link was clicked
    url: string;           // The clicked URL
    userAgent?: string;    // Browser/client user agent
    ip?: string;           // IP address
    platform?: string;     // Detected platform
  }
}

Contact events

All contact events (contact.created, contact.updated, contact.deleted) include:
{
  id: string;              // Contact ID
  email: string;           // Contact email address
  contactBookId: string;   // Contact book ID
  subscribed: boolean;     // Subscription status
  properties: object;      // Custom properties
  firstName?: string;      // First name
  lastName?: string;       // Last name
  createdAt: string;       // ISO 8601 timestamp
  updatedAt: string;       // ISO 8601 timestamp
}

Domain events

All domain events (domain.created, domain.verified, domain.updated, domain.deleted) include:
{
  id: number;              // Domain ID
  name: string;            // Domain name (e.g., "example.com")
  status: string;          // Domain status
  region: string;          // AWS region
  createdAt: string;       // ISO 8601 timestamp
  updatedAt: string;       // ISO 8601 timestamp
  clickTracking: boolean;  // Click tracking enabled
  openTracking: boolean;   // Open tracking enabled
  subdomain?: string;      // Subdomain for tracking
  dkimStatus?: string;     // DKIM verification status
  spfDetails?: string;     // SPF record details
  dmarcAdded?: boolean;    // DMARC record added
}