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

# Webhooks

> Real-time event notifications to your server

## Overview

Webhooks allow you to receive real-time HTTP notifications when events occur in your AppDNA application. When a subscribed event fires, AppDNA sends a POST request to your configured endpoint with a JSON payload describing the event.

***

## Supported Event Types

AppDNA supports 16 webhook event types:

| Event Type              | Trigger                                     |
| ----------------------- | ------------------------------------------- |
| `onboarding.started`    | User begins an onboarding flow              |
| `onboarding.completed`  | User completes an onboarding flow           |
| `survey.completed`      | User submits a survey response              |
| `payment.completed`     | Purchase transaction succeeds               |
| `payment.failed`        | Purchase transaction fails                  |
| `subscription.canceled` | User cancels a subscription                 |
| `push.delivered`        | Push notification delivered to device       |
| `push.opened`           | User taps a push notification               |
| `email.opened`          | User opens an email                         |
| `email.clicked`         | User clicks a link in an email              |
| `message.clicked`       | User interacts with an in-app message       |
| `journey.completed`     | User completes a journey/lifecycle flow     |
| `journey.exited`        | User exits a journey before completion      |
| `experiment.exposure`   | User is exposed to an experiment variant    |
| `user.identified`       | Anonymous user is linked to a known user ID |
| `webhook.test`          | Test event sent from the dashboard          |

***

## Payload Format

Every webhook delivery sends a JSON payload with the following structure:

```json theme={null}
{
  "id": "evt-550e8400-e29b-41d4-a716-446655440000",
  "type": "payment.completed",
  "created_at": "2026-02-19T10:00:00Z",
  "data": {
    "user_id": "user-123",
    "product_id": "premium_monthly",
    "transaction_id": "txn-456",
    "price": 9.99,
    "currency": "USD",
    "platform": "ios"
  }
}
```

| Field        | Type                | Description                                   |
| ------------ | ------------------- | --------------------------------------------- |
| `id`         | `string (uuid)`     | Unique event identifier                       |
| `type`       | `string`            | One of the 16 supported event types           |
| `created_at` | `string (datetime)` | ISO 8601 timestamp of when the event occurred |
| `data`       | `object`            | Event-specific payload (varies by event type) |

***

## Request Headers

Every webhook request includes the following headers:

| Header                 | Description                                  |
| ---------------------- | -------------------------------------------- |
| `Content-Type`         | `application/json`                           |
| `X-AppDNA-Signature`   | HMAC-SHA256 signature for verification       |
| `X-AppDNA-Event`       | Event type (e.g., `payment.completed`)       |
| `X-AppDNA-Delivery-Id` | Unique delivery identifier for deduplication |
| `X-AppDNA-Timestamp`   | Unix timestamp of when the request was sent  |
| `User-Agent`           | `AppDNA-Webhooks/1.0`                        |

***

## Signature Verification

Every webhook request is signed with your endpoint secret using HMAC-SHA256. Always verify the signature before processing the payload.

The signature is sent in the `X-AppDNA-Signature` header in the format:

```
X-AppDNA-Signature: sha256=<hex-encoded-signature>
```

The signing input is the raw JSON stringified request body.

<Tabs>
  <Tab title="Node.js">
    ```javascript theme={null}
    const crypto = require('crypto');

    function verifyWebhookSignature(payload, signature, secret) {
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');

      const providedSignature = signature.replace('sha256=', '');

      return crypto.timingSafeEqual(
        Buffer.from(expectedSignature, 'hex'),
        Buffer.from(providedSignature, 'hex')
      );
    }

    // Express.js example
    app.post('/webhooks/appdna', express.raw({ type: 'application/json' }), (req, res) => {
      const signature = req.headers['x-appdna-signature'];
      const payload = req.body.toString();

      if (!verifyWebhookSignature(payload, signature, process.env.APPDNA_WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
      }

      const event = JSON.parse(payload);
      console.log(`Received ${event.type}:`, event.data);

      res.status(200).send('OK');
    });
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import hmac
    import hashlib

    def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
        expected = hmac.new(
            secret.encode('utf-8'),
            payload,
            hashlib.sha256
        ).hexdigest()

        provided = signature.replace('sha256=', '')

        return hmac.compare_digest(expected, provided)

    # Flask example
    from flask import Flask, request, abort

    app = Flask(__name__)

    @app.route('/webhooks/appdna', methods=['POST'])
    def handle_webhook():
        signature = request.headers.get('X-AppDNA-Signature', '')
        payload = request.get_data()

        if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
            abort(401)

        event = request.get_json()
        print(f"Received {event['type']}: {event['data']}")

        return 'OK', 200
    ```
  </Tab>
</Tabs>

<Warning>
  Always use constant-time comparison (e.g., `crypto.timingSafeEqual` in Node.js or `hmac.compare_digest` in Python) when verifying signatures. Simple string comparison is vulnerable to timing attacks.
</Warning>

***

## Retry Policy

If your endpoint returns a non-2xx response or times out, AppDNA retries the delivery with exponential backoff:

| Retry | Delay      |
| ----- | ---------- |
| 1st   | 30 seconds |
| 2nd   | 2 minutes  |
| 3rd   | 10 minutes |
| 4th   | 1 hour     |

**Maximum retries:** 5 attempts total (1 initial delivery + 4 retries).

**Delivery timeout:** 30 seconds. If your server does not respond within 30 seconds, the delivery is marked as failed and scheduled for retry.

**Maximum response body logged:** 10 KB. Response bodies exceeding this limit are truncated in delivery logs.

<Note>
  After 50 consecutive delivery failures, the webhook endpoint is automatically disabled. You can re-enable it from the dashboard or via the API.
</Note>

***

## Management API

All management endpoints require Customer JWT authentication (`Authorization: Bearer` header).

### List Endpoints

```
GET /api/v1/webhooks/endpoints
```

Returns all configured webhook endpoints for the current application.

### Create Endpoint

```
POST /api/v1/webhooks/endpoints
```

**Request body:**

```json theme={null}
{
  "name": "Production Backend",
  "url": "https://api.example.com/webhooks/appdna",
  "events": ["payment.completed", "payment.failed", "subscription.canceled"],
  "description": "Handles payment lifecycle events",
  "headers": {
    "X-Custom-Header": "custom-value"
  }
}
```

| Field         | Type       | Required | Description                             |
| ------------- | ---------- | -------- | --------------------------------------- |
| `name`        | `string`   | Yes      | Human-readable endpoint name            |
| `url`         | `string`   | Yes      | Destination URL (HTTPS required)        |
| `events`      | `string[]` | Yes      | Array of event types to subscribe to    |
| `description` | `string`   | No       | Optional description                    |
| `headers`     | `object`   | No       | Custom headers to include in deliveries |

<Warning>
  Webhook URLs must use HTTPS. HTTP endpoints are rejected.
</Warning>

### Update Endpoint

```
PATCH /api/v1/webhooks/endpoints/{id}
```

Update any field on an existing endpoint. Partial updates are supported.

### Delete Endpoint

```
DELETE /api/v1/webhooks/endpoints/{id}
```

Permanently deletes the endpoint and all associated delivery history.

### Test Endpoint

```
POST /api/v1/webhooks/endpoints/{id}/test
```

Sends a `webhook.test` event to the endpoint. Use this to verify your endpoint is reachable and correctly verifying signatures.

### Rotate Signing Secret

```
POST /api/v1/webhooks/endpoints/{id}/rotate-secret
```

Generates a new signing secret for the endpoint. The previous secret is immediately invalidated.

<Info>
  After rotating a secret, update your server's verification code with the new secret before the next delivery attempt.
</Info>

### Disable Endpoint

```
POST /api/v1/webhooks/endpoints/{id}/disable
```

Temporarily disables the endpoint. No deliveries are attempted while disabled. Events that occur during this period are not queued.

### Enable Endpoint

```
POST /api/v1/webhooks/endpoints/{id}/enable
```

Re-enables a previously disabled endpoint.

### List Deliveries

```
GET /api/v1/webhooks/endpoints/{id}/deliveries
```

Returns a paginated list of delivery attempts for the endpoint, including status, response code, and timestamps.

### Delivery Detail

```
GET /api/v1/webhooks/deliveries/{id}
```

Returns full details for a specific delivery attempt, including request/response headers and body.

### Retry Failed Delivery

```
POST /api/v1/webhooks/deliveries/{id}/retry
```

Manually retries a failed delivery. The retry is attempted immediately, independent of the automatic retry schedule.

### Event Catalog

```
GET /api/v1/webhooks/events
```

Returns the full list of supported webhook event types with descriptions.

<Check>
  Your webhook integration is working correctly when you receive a `webhook.test` event at your endpoint and can successfully verify the signature.
</Check>
