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

> Receive real-time event notifications via HTTP callbacks

# Webhooks

AppDNA sends HTTP POST requests to your server when events occur in your app. Webhooks let you sync data to your backend, trigger workflows, update CRMs, feed events into data warehouses, or power any custom integration.

***

## Overview

When you create a webhook endpoint in the AppDNA dashboard, you choose which event types to subscribe to. When a matching event occurs, AppDNA delivers a signed JSON payload to your endpoint URL via HTTP POST.

Every delivery includes an HMAC-SHA256 signature so you can verify the payload originated from AppDNA and was not tampered with.

***

## Setup

<Steps>
  <Step title="Navigate to Webhooks">
    Go to **Console > Settings > Webhooks** in the AppDNA dashboard.
  </Step>

  <Step title="Add an endpoint">
    Click **Create Endpoint** and enter your HTTPS URL. Non-HTTPS URLs are not supported in production.
  </Step>

  <Step title="Select event types">
    Choose which event types this endpoint should receive. You can select individual events or subscribe to all events.
  </Step>

  <Step title="Save and copy the signing secret">
    After saving, the dashboard displays your **signing secret**. Copy it immediately -- it is only shown once. You will use this secret to verify webhook signatures.
  </Step>
</Steps>

<Warning>
  The signing secret is only displayed once when you create the endpoint. If you lose it, you must rotate the secret from the endpoint settings, which invalidates the previous secret.
</Warning>

***

## Event Types

AppDNA supports 16 webhook event types organized by category:

### Onboarding

| Event                  | Description                      |
| ---------------------- | -------------------------------- |
| `onboarding.started`   | User started an onboarding flow  |
| `onboarding.completed` | User finished an onboarding flow |

### Surveys

| Event              | Description             |
| ------------------ | ----------------------- |
| `survey.completed` | User completed a survey |

### Billing

| Event                   | Description                                |
| ----------------------- | ------------------------------------------ |
| `payment.completed`     | Subscription purchase or renewal succeeded |
| `payment.failed`        | Payment attempt failed                     |
| `subscription.canceled` | User canceled their subscription           |

### Push

| Event            | Description                           |
| ---------------- | ------------------------------------- |
| `push.delivered` | Push notification delivered to device |
| `push.opened`    | User tapped push notification         |

### Email

| Event           | Description                     |
| --------------- | ------------------------------- |
| `email.opened`  | User opened an email            |
| `email.clicked` | User clicked a link in an email |

### In-App Messages

| Event             | Description                          |
| ----------------- | ------------------------------------ |
| `message.clicked` | User tapped an in-app message action |

### Journeys

| Event               | Description                 |
| ------------------- | --------------------------- |
| `journey.completed` | User completed a journey    |
| `journey.exited`    | User exited a journey early |

### Experiments

| Event                 | Description                           |
| --------------------- | ------------------------------------- |
| `experiment.exposure` | User exposed to an experiment variant |

### SDK

| Event             | Description                 |
| ----------------- | --------------------------- |
| `user.identified` | User identified via the SDK |

### System

| Event          | Description                 |
| -------------- | --------------------------- |
| `webhook.test` | Test event (manual trigger) |

***

## Payload Structure

Every webhook payload follows a consistent structure:

```json theme={null}
{
  "id": "evt_01HYX9K3M7N8P2Q4R5S6T7V8W9",
  "type": "payment.completed",
  "created_at": "2026-02-19T10:30:00Z",
  "data": {
    "user_id": "usr_123",
    "product_id": "prod_premium",
    "transaction_id": "txn_xyz",
    "price": 9.99,
    "currency": "USD",
    "platform": "ios"
  }
}
```

| Field        | Type     | Description                                          |
| ------------ | -------- | ---------------------------------------------------- |
| `id`         | `string` | Unique event ID. Use this for idempotency.           |
| `type`       | `string` | The event type (e.g., `payment.completed`).          |
| `created_at` | `string` | ISO 8601 timestamp of when the event occurred.       |
| `data`       | `object` | Event-specific payload. Contents vary by event type. |

<Note>
  The `data` object contents vary by event type. Refer to the specific event type documentation in the dashboard for the full schema of each event.
</Note>

***

## Signature Verification

Every webhook request includes an `x-appdna-signature` header containing an HMAC-SHA256 signature of the raw request body. Always verify this signature before processing the payload.

The signature is computed as:

```
HMAC-SHA256(signing_secret, raw_request_body)
```

The header value is hex-encoded and prefixed with `sha256=`:

```
x-appdna-signature: sha256=a1b2c3d4e5f6...
```

### Verification Examples

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

    function verifyWebhook(req, signingSecret) {
      const signature = req.headers['x-appdna-signature'];
      if (!signature) return false;

      const expected = 'sha256=' + crypto
        .createHmac('sha256', signingSecret)
        .update(req.rawBody)
        .digest('hex');

      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expected)
      );
    }

    // Express middleware example
    app.post('/webhooks/appdna', express.raw({ type: 'application/json' }), (req, res) => {
      if (!verifyWebhook(req, process.env.APPDNA_WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
      }

      const event = JSON.parse(req.body);
      console.log(`Received ${event.type}: ${event.id}`);

      // Process the event asynchronously
      processEvent(event).catch(console.error);

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

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

    def verify_webhook(payload: bytes, signature: str, signing_secret: str) -> bool:
        if not signature:
            return False

        expected = 'sha256=' + hmac.new(
            signing_secret.encode('utf-8'),
            payload,
            hashlib.sha256
        ).hexdigest()

        return hmac.compare_digest(signature, expected)

    # Flask example
    @app.route('/webhooks/appdna', methods=['POST'])
    def handle_webhook():
        signature = request.headers.get('x-appdna-signature')
        if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
            return 'Invalid signature', 401

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

        # Process asynchronously (e.g., enqueue to Celery)
        process_event.delay(event)

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

  <Tab title="Ruby">
    ```ruby theme={null}
    require 'openssl'

    def verify_webhook(payload, signature, signing_secret)
      return false unless signature

      expected = 'sha256=' + OpenSSL::HMAC.hexdigest(
        'SHA256',
        signing_secret,
        payload
      )

      Rack::Utils.secure_compare(signature, expected)
    end

    # Sinatra example
    post '/webhooks/appdna' do
      payload = request.body.read
      signature = request.env['HTTP_X_APPDNA_SIGNATURE']

      unless verify_webhook(payload, signature, ENV['APPDNA_WEBHOOK_SECRET'])
        halt 401, 'Invalid signature'
      end

      event = JSON.parse(payload)
      puts "Received #{event['type']}: #{event['id']}"

      # Process asynchronously
      ProcessEventJob.perform_async(event)

      status 200
      'OK'
    end
    ```
  </Tab>
</Tabs>

<Warning>
  Always use a constant-time comparison function (e.g., `crypto.timingSafeEqual`, `hmac.compare_digest`, `Rack::Utils.secure_compare`) to prevent timing attacks.
</Warning>

***

## Retry Policy

If your endpoint returns a non-2xx status code or does not respond within 30 seconds, AppDNA retries the delivery with exponential backoff:

| Retry | Delay After Failure |
| ----- | ------------------- |
| 1     | 30 seconds          |
| 2     | 2 minutes           |
| 3     | 10 minutes          |
| 4     | 1 hour              |

After the initial attempt plus 4 retries (5 attempts total), that delivery is marked as failed. You can view failed deliveries and manually retry them from the dashboard.

After **50 consecutive failures** across any events, the webhook endpoint is **automatically disabled**. You can re-enable the endpoint from the dashboard after fixing the issue.

***

## Best Practices

1. **Respond with 2xx quickly.** Return a `200 OK` as soon as you receive the payload. Process the event asynchronously in a background job or queue. If your endpoint takes too long to respond, the delivery will be marked as failed and retried.

2. **Verify signatures on every request.** Never process a webhook payload without verifying the `x-appdna-signature` header. This protects you from forged requests.

3. **Handle idempotency.** Webhook deliveries can be retried, which means your endpoint may receive the same event more than once. Use the `id` field to deduplicate events. Store processed event IDs and skip duplicates.

4. **Use HTTPS.** AppDNA only delivers webhooks to HTTPS endpoints in production. During development, you can use a tunneling tool like ngrok to expose a local endpoint.

5. **Monitor delivery health.** Check the webhook delivery logs in the dashboard periodically. If you see a pattern of failures, investigate your endpoint's availability and response times.

<Info>
  To test webhooks locally during development, use a tunneling tool like [ngrok](https://ngrok.com) and point your webhook endpoint to the tunnel URL. Sandbox webhooks (`adn_test_` environment) are fully functional and isolated from production.
</Info>
