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

> ## Agent Instructions
> AhaSend stores all message data in the EU. See /facts.
> The API base URL is https://api.ahasend.com. Send the API key in the Authorization: Bearer header. See /api-reference/authentication.
> Use send.ahasend.com for SMTP. Ports 25, 587 and 2525 use STARTTLS. Port 465 is not supported. See /smtp.
> For send-only integrations, create an API Key v2 under Credentials → Add and scope it to messages:send:{your-domain}. See /send-api/credentials and /api-reference/scopes.

# Email Webhooks: Delivery, Bounce and Open Events

> Receive real-time AhaSend email events (delivered, bounced, deferred, opened, clicked) with webhooks, verify signatures and handle retries.

AhaSend webhooks send email events to your application as HTTP requests; this guide shows how to create an endpoint, choose events and test delivery.

Available on Free, Pro and Max. See [plans and features](https://ahasend.com/docs/facts.md#plans-and-features).

> [!NOTE]
>
> **Real-Time Integration:** Unlike polling APIs that require you to repeatedly check for updates, webhooks deliver event data instantly as it happens, making your applications more responsive and efficient.

## What Are AhaSend Webhooks?

Webhooks are user-defined HTTP callbacks triggered by specific events in AhaSend. When an event occurs—such as an email being delivered or bouncing—AhaSend automatically sends a POST request to your webhook URL with detailed event data.

This enables you to:

> [card] Automate Workflows
>
> Trigger actions in your application when emails are delivered or bounce

> [card] Monitor Performance
>
> Track email engagement in real-time without manual checking

> [card] Update User Interfaces
>
> Show delivery status updates to users immediately

> [card] Handle Errors
>
> Respond to delivery failures or suppression events automatically

### How Webhooks Work in AhaSend

**1. Event Occurs**

An event happens in your account (email delivered, bounced, opened, etc.)

**2. Webhook Triggered**

AhaSend identifies any webhooks configured for that event type

**3. HTTP Request Sent**

AhaSend sends a POST request to your webhook URL with event data

**4. Your Application Responds**

Your endpoint processes the event and returns a 2xx status code

> [!NOTE]
>
> **Delivery Guarantee:** AhaSend makes 6 attempts over about 16 minutes, including the first attempt. Only HTTP status codes 200-299 are considered successful.

## Which Events Can I Receive?

Choose delivery, bounce, deferral, failure, suppression, open, click, unsubscribe, spam complaint, contact and domain-DNS events when configuring your webhook. Contact events are available to accounts with the marketing features. The [event reference](https://ahasend.com/docs/api-reference/webhooks.md#event-types) lists every event, its filter flag and its payload. Inbound routes use the separate [`message.routing` event](https://ahasend.com/docs/api-reference/routes/route-message).

## Understanding Message Lifecycle

The message lifecycle helps you understand when different webhook events occur:

### Successful Delivery Path

```text theme={null}
Reception → Delivered → [Opened] → [Clicked]
```

1. **Reception**: Email accepted and queued for delivery
2. **Delivered**: Successfully sent to recipient's mail server
3. **Opened**: Recipient opens email (requires tracking enabled)
4. **Clicked**: Recipient clicks tracked links (requires tracking enabled)

### Bounce Scenarios

> [accordion] Hard Bounce (Immediate Failure)
>
> ```text theme={null}
> Reception → Bounced → Suppression Created
> ```
>
> Occurs when the recipient's server immediately rejects the email (invalid address, domain doesn't exist). The email address is automatically suppressed.

> [accordion] Soft Bounce with Recovery
>
> ```text theme={null}
> Reception → Deferred → Deferred → ... → Delivered
> ```
>
> Temporary issues (full mailbox, server problems) cause deferrals. Message delivery retries continue until the message's expiry time. This is separate from [webhook delivery retries](https://ahasend.com/docs/api-reference/webhooks/retry-policy.md).

> [accordion] Soft Bounce with Final Failure
>
> ```text theme={null}
> Reception → Deferred → Deferred → ... → Failed → Suppression Created
> ```
>
> After the retry window is exhausted, the message fails permanently and the address may be suppressed. Because retry exhaustion is not one SMTP exchange, its terminal event has no `delivery_attempt` object.

> [accordion] Suppressed Email
>
> ```text theme={null}
> Reception → Suppressed
> ```
>
> No delivery attempt is made because the recipient is on your suppression list from previous failures.

## How Do I Create a Webhook?

![Screenshot of the webhooks dashboard](https://ahasend.com/sites/default/files/inline-images/webhooks.png)

Configure webhooks in your AhaSend dashboard to start receiving event notifications:

**1. Access Webhooks Dashboard**

1. **Log in** to your [AhaSend Dashboard](https://dash.ahasend.com)
2. **Navigate** to the **[Webhooks](https://dash.ahasend.com/account/-/integrations/webhooks)** section from the main menu
3. **Click** the **"Add Webhook"** button

**2. Configure Webhook URL**

**Enter Your Endpoint URL:**

* Provide the complete URL where you want to receive webhook events
* Must be a valid HTTPS URL (HTTP allowed for development)
* Should respond with 2xx status codes for successful processing

**Example URLs:**

```text theme={null}
https://api.yourapp.com/webhooks/ahasend
https://yourapp.com/api/email-events
```

**3. Select Events**

Choose which events to receive:

**Option 1: All Events**

* Select "Send all events to this endpoint URL"
* Simplest option, receives every event type
* Good for comprehensive logging or analytics

**Option 2: Specific Events**

* Select "I'll choose which events to send"
* Pick individual event types you need
* More efficient for targeted use cases

**Send changes made by:** Choose whose contact and suppression changes the webhook gets: the recipient, the API, the dashboard or AhaSend. See [origin](https://ahasend.com/docs/api-reference/webhooks.md#origin).

**4. Create and Verify**

1. **Click** "Create Webhook" to save your configuration
2. **Note the Webhook Secret** displayed on the details page
3. **Copy and store** the secret securely for signature verification

## How Do I Test a Webhook Locally?

Use [CLI webhook testing](https://ahasend.com/docs/cli/webhook-testing.md) to forward events to localhost and trigger sample events. Use [sandbox sends](https://ahasend.com/docs/send-api/sandbox.md) to exercise delivery outcomes without real mail.

Verify your webhook integration works correctly before going live:

**1. Use Test Events**

1. **Go to** your webhook details page in the dashboard
2. **Click** "Send Test Event"
3. **Select** which event types to test
4. **Click** "Send Test Events"

Test events arrive within seconds and contain realistic sample data. Test
`message.delivered`, `message.transient_error`, and `message.bounced` events
include representative `delivery_attempt` diagnostics; these values are
synthetic and were not observed from a destination mail server.

**2. Development Tools**

**For Quick Testing:**

* Use [Beeceptor](https://beeceptor.com) to create temporary webhook URLs
* Inspect incoming requests and verify payload structure
* Test without writing code first

**For Development:**

* Use [ngrok](https://ngrok.com) to expose local development servers
* Test webhook handling in your actual application code

**3. Verify Response Handling**

Ensure your endpoint:

* **Returns 2xx status codes** for successful processing
* **Processes requests quickly** (under 5 seconds)
* **Handles duplicate events** gracefully using webhook-id
* **Validates webhook signatures** for security

> [!WARNING]
>
> **Timeout Considerations:** Webhook requests timeout after 5 seconds. Perform heavy processing asynchronously.

## How Do I Verify a Webhook?

AhaSend follows the [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks) for secure webhook delivery:

`webhook-id` is the delivery ID used for deduplication. The payload's `webhook_id` or `route_id` identifies the configured endpoint, not the delivery.

## Security Headers

Every webhook request includes security headers:

```http theme={null}
webhook-id: wh_1234567890abcdef
webhook-timestamp: 1683360000
webhook-signature: v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=
```

> [accordion] webhook-id
>
> **Unique event identifier** - Use as an idempotency key to prevent processing duplicate events from retries.

> [accordion] webhook-timestamp
>
> **Unix timestamp** when the webhook was sent - Use to reject old webhook attempts.

> [accordion] webhook-signature
>
> **HMAC signature** of the payload using your webhook secret - Verify this to ensure authenticity.

### Signature Verification

Use Standard Webhooks libraries for easy verification:

```javascript Node.js theme={null}
import { Webhook } from 'standardwebhooks';

async function readLimitedBody(request) {
  const reader = request.body?.getReader();
  if (!reader) return new Uint8Array();
  const chunks = [];
  let size = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    size += value.byteLength;
    if (size > 30_000_000) { await reader.cancel(); return null; }
    chunks.push(value);
  }
  const body = new Uint8Array(size);
  let offset = 0;
  for (const chunk of chunks) { body.set(chunk, offset); offset += chunk.byteLength; }
  return body;
}

const webhook = new Webhook(process.env.WEBHOOK_SECRET, { format: 'raw' });

// In your webhook handler
export async function handleWebhook(request) {
  const payload = await readLimitedBody(request);
  if (payload === null) return new Response(null, { status: 413 });
  const headers = {
    'webhook-id': request.headers.get('webhook-id'),
    'webhook-timestamp': request.headers.get('webhook-timestamp'),
    'webhook-signature': request.headers.get('webhook-signature')
  };

  try {
    const event = webhook.verify(new TextDecoder('utf-8', { fatal: true }).decode(payload), headers);
    // Process verified event
    console.log('Verified event:', event.type); // Includes unknown signed event types.
    return new Response('OK', { status: 200 });
  } catch (error) {
    return new Response('Invalid signature', { status: 400 });
  }
}
```

The Python example expects a framework request with a raw `body` attribute. Configure a 30,000,000-byte request limit in that framework or its front proxy before it buffers the body. Supply the application processing callback used below.

```python Python theme={null}
import base64
import binascii

def supported_signatures(header):
    # Skip unsupported or malformed entries before the library decodes them.
    supported = []
    for entry in (header or "").split():
        version, separator, encoded = entry.partition(",")
        if version != "v1" or not separator:
            continue
        try:
            signature = base64.b64decode(encoded, validate=True)
        except (binascii.Error, ValueError):
            continue
        if len(signature) == 32:
            supported.append(entry)
    return " ".join(supported)

from standardwebhooks.webhooks import Webhook
import os

webhook = Webhook(os.environ['WEBHOOK_SECRET'].encode('utf-8'))

def handle_webhook(request):
    headers = {
        'webhook-id': request.headers.get('webhook-id'),
        'webhook-timestamp': request.headers.get('webhook-timestamp'),
        'webhook-signature': supported_signatures(request.headers.get('webhook-signature'))
    }

    try:
        payload = request.body
        if len(payload) > 30_000_000:
            return {'error': 'Request too large'}, 413
        event = webhook.verify(payload, headers)

        # Process verified event
        print(f"Verified event: {event['type']}")
        return {'status': 'success'}, 200
    except Exception as e:
        return {'error': 'Invalid signature'}, 400
```

```go Go theme={null}
package main

import (
    "io"
    "log"
    "net/http"
    "os"
    standardwebhooks "github.com/standard-webhooks/standard-webhooks/libraries/go"
)

func handleWebhook(w http.ResponseWriter, r *http.Request) {
    webhook, err := standardwebhooks.NewWebhookRaw([]byte(os.Getenv("WEBHOOK_SECRET")))
    if err != nil {
        http.Error(w, "Invalid webhook secret", 500)
        return
    }

    r.Body = http.MaxBytesReader(w, r.Body, 30_000_000)
    payload, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Cannot read webhook body", 413)
        return
    }
    headers := r.Header

    if err := webhook.Verify(payload, headers); err != nil {
        http.Error(w, "Invalid signature", 400)
        return
    }

    // Process verified event
    log.Printf("Verified webhook received")
    w.WriteHeader(200)
}
```

```php PHP theme={null}
<?php
// PHP handler: read the exact body, with a limit, before parsing JSON.
$body = file_get_contents('php://input', false, null, 0, 30_000_001);
$secret = getenv('WEBHOOK_SECRET');
$id = $_SERVER['HTTP_WEBHOOK_ID'] ?? '';
$timestamp = $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
$signatures = $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '';
if ($body === false || strlen($body) > 30_000_000) { http_response_code(413); exit; }
if (!$secret) { http_response_code(500); exit; }
if (!$id || !ctype_digit($timestamp) || abs(time() - (int)$timestamp) > 300) {
    http_response_code(400); exit;
}
$expected = base64_encode(hash_hmac('sha256', "$id.$timestamp.$body", $secret, true));
$valid = false;
foreach (explode(' ', $signatures) as $candidate) {
    $parts = explode(',', $candidate, 2);
    if (count($parts) === 2 && $parts[0] === 'v1' && hash_equals($expected, $parts[1])) $valid = true;
}
if (!$valid) { http_response_code(400); exit; }
try { $event = json_decode($body, true, 512, JSON_THROW_ON_ERROR); }
catch (JsonException $e) { http_response_code(400); exit; }
// Verification-only example: add an atomic durable handoff before business side effects.
// Unknown event types are valid signed deliveries too.
http_response_code(204);
```

> [!NOTE]
>
> **Standard Webhooks Libraries:** AhaSend webhooks are fully compatible with [Standard Webhooks libraries](https://github.com/standard-webhooks/standard-webhooks) available for Python, JavaScript, Go, PHP, Ruby, Java, Rust, C#, and Elixir.

## Webhook Payload Structure

> [!TIP]
>
> **Complete Payload Documentation:** For detailed event schemas and all payload fields, see the [Webhook Events API Reference](https://ahasend.com/docs/api-reference/webhooks.md).

All AhaSend webhooks follow the Standard Webhooks payload format:

```json theme={null}
{
  "type": "event.type",
  "webhook_id": "abe11757-2886-4b55-96f1-0e0afc95795a",
  "timestamp": "2024-05-06T09:49:16.687031577Z",
  "data": {
    // Event-specific data
  }
}
```

### Message Event Example

```json theme={null}
{
  "type": "message.delivered",
  "webhook_id": "abe11757-2886-4b55-96f1-0e0afc95795a",
  "timestamp": "2024-05-06T09:50:16.687031577Z",
  "data": {
    "account_id": "4cdd7bdd-294e-4762-892f-83d40abf5a87",
    "event": "on_delivered",
    "from": "hello@yourcompany.com",
    "recipient": "customer@example.com",
    "subject": "Welcome to our service",
    "message_id_header": "message-id-12345",
    "id": "ahasend-message-67890",
    "delivery_attempt": {
      "smtp_code": 250,
      "enhanced_status_code": "2.0.0",
      "response": "OK: queued",
      "command": "DATA"
    }
  }
}
```

### Delivery Attempt Diagnostics

`message.delivered`, `message.transient_error`, and `message.bounced` can include
an optional `data.delivery_attempt` object describing the SMTP exchange behind
the event:

* `smtp_code` is always present when the object is present. A value of `0`
  means AhaSend recorded diagnostic text without an SMTP code.
* `enhanced_status_code` is the enhanced status code, such as `5.1.1`, when
  one was recorded.
* `response` contains human-readable diagnostic text. Do not parse identifiers
  from it.
* `description` can contain a clearer translation of a complex response. Its
  wording may change, so use it only for display.
* `classification` is the machine-readable failure category. Treat it as an
  open set and keep a fallback for values your integration does not recognize.
* `command` is the normalized SMTP command name, such as `RCPT TO` or `DATA`.

Do not require this object. A missing or `null` value is normal for retry
exhaustion, out-of-band bounces, non-SMTP routing, and attempts with no recorded
response. In particular, retry exhaustion is reported as `message.failed`,
which has no specific attempt to report.

Test webhooks and sandbox sends use representative diagnostics rather than an
observed SMTP exchange. A sandbox classification may not agree with its sample
SMTP code and response, so test classification-based behavior with production
events or fixtures you control.

### Suppression Event Example

```json theme={null}
{
  "type": "suppression.created",
  "webhook_id": "abe11757-2886-4b55-96f1-0e0afc95795a",
  "timestamp": "2024-05-06T12:57:06.451529527Z",
  "data": {
    "account_id": "4cdd7bdd-294e-4762-892f-83d40abf5a87",
    "origin": "system",
    "recipient": "bounced@example.com",
    "created_at": "2024-05-06T12:57:06.451529617Z",
    "expires_at": "2024-06-05T12:57:06.451529617Z",
    "reason": "Invalid Recipient",
    "sending_domain": "yourcompany.com"
  }
}
```

## Best Practices

> [accordion] Reliability and Performance
>
> **Handle Retries Gracefully:**
>
> * Use `webhook-id` as an idempotency key to prevent duplicate processing
> * Store processed webhook IDs to detect and skip duplicates
> * Return 2xx status codes quickly (under 5 seconds)
>
> **Process Asynchronously:**
>
> * Acknowledge receipt immediately with 200 OK
> * Queue heavy processing for background workers
> * Avoid database operations that could timeout
>
> **Monitor Your Endpoints:**
>
> * Log webhook processing times and errors
> * Set up alerts for failing webhooks
> * Track webhook-related application metrics

> [accordion] Security Implementation
>
> **Always Verify Signatures:**
>
> * Use Standard Webhooks libraries for verification
> * Reject requests with invalid or missing signatures
> * Check timestamp to prevent replay attacks
>
> **Secure Your Endpoints:**
>
> * Use HTTPS URLs for production webhooks
> * Don't expose webhook URLs publicly
> * Consider IP whitelisting if needed
>
> **Protect Webhook Secrets:**
>
> * Store webhook secrets in environment variables
> * Never commit secrets to version control
> * Rotate secrets periodically

> [accordion] Error Handling
>
> **Graceful Failure Handling:**
>
> * Return appropriate HTTP status codes
> * Log webhook processing errors for debugging
> * Implement circuit breakers for external dependencies
>
> **Event Processing Logic:**
>
> * Handle unexpected event types gracefully
> * Validate event data before processing
> * Don't assume all fields will always be present
>
> **Testing and Monitoring:**
>
> * Test webhook handlers with various event types
> * Monitor webhook delivery success rates in dashboard
> * Set up alerts for consecutive webhook failures

## Why Did My Webhook Fail?

> [accordion] Webhooks Not Being Received
>
> **Check Your Configuration:**
>
> * Verify webhook URL is correct and accessible
> * Ensure your server returns 2xx status codes
> * Check firewall settings aren't blocking AhaSend IPs
>
> **Test Your Endpoint:**
>
> * Use "Send Test Event" feature in dashboard
> * Test with tools like curl or Postman
> * Verify your endpoint handles POST requests
>
> **Review Dashboard Logs:**
>
> * Check webhook delivery attempts in your dashboard
> * Look for error messages and status codes
> * Monitor retry attempts and failure patterns

> [accordion] Authentication Errors
>
> **Signature Verification Issues:**
>
> * Ensure webhook secret is correctly configured
> * Use raw request body for signature calculation
> * Check Standard Webhooks library implementation
>
> **Common Mistakes:**
>
> * Using wrong webhook secret
> * Modifying request body before verification
> * Incorrect header name extraction
>
> **Debug Steps:**
>
> * Log incoming headers and payload
> * Verify webhook secret matches dashboard
> * Test with Standard Webhooks library examples

> [accordion] Performance Issues
>
> **Timeout Problems:**
>
> * Reduce processing time in webhook handler
> * Move heavy operations to background jobs
> * Return 200 OK before processing starts
>
> **High Volume Handling:**
>
> * Implement proper queuing systems
> * Scale webhook processing horizontally
> * Monitor resource usage during peak times
>
> **Optimization Tips:**
>
> * Cache frequently accessed data
> * Use database connection pooling
> * Implement proper logging levels

> [accordion] Webhook Disabled
>
> **Automatic Disabling:**
>
> * AhaSend disables webhooks after more than 100 consecutive failed attempts
> * You'll receive an email notification when this happens
> * Fix underlying issues before re-enabling
>
> **Re-enabling Process:**
>
> * Identify and resolve the root cause
> * Test your endpoint is working correctly
> * Re-enable the webhook in your dashboard
>
> **Prevention:**
>
> * Monitor webhook success rates regularly
> * Set up alerts for webhook failures
> * Implement proper error handling and logging

## Advanced Integration Patterns

> [card] Event-Driven Architecture
>
> Use webhooks to trigger microservices, update user interfaces, and orchestrate complex workflows based on email events.

> [card] Real-Time Analytics
>
> Stream webhook events to analytics platforms for real-time email performance dashboards and reporting.

> [card] Customer Support Integration
>
> Automatically create support tickets from bounce events and delivery failures to keep your team informed.

> [card] Marketing Automation
>
> Trigger marketing campaigns, update customer segments, and personalize user experiences based on email engagement.

## Related Guides

* [Domain setup](https://ahasend.com/docs/domains.md), [API authentication](https://ahasend.com/docs/api-reference/authentication.md), [Event reference](https://ahasend.com/docs/api-reference/webhooks.md).
* [Webhook CLI](https://ahasend.com/docs/cli/commands/webhooks.md), [Local webhook testing](https://ahasend.com/docs/cli/webhook-testing.md), [Signature verification](https://ahasend.com/docs/api-reference/webhooks/security.md).
* [Retry policy](https://ahasend.com/docs/api-reference/webhooks/retry-policy.md), [Node.js SDK](https://ahasend.com/docs/guides/nodejs-sdk.md), [Go SDK](https://ahasend.com/docs/guides/go-sdk.md).
* [Plans and limits](https://ahasend.com/docs/facts.md), [Tracking setup](https://ahasend.com/docs/tracking/open-tracking.md), [Sandbox behavior](https://ahasend.com/docs/send-api/sandbox.md).
* [Suppression durations](https://ahasend.com/docs/bounces.md#suppression-duration).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.