# Webhooks Overview
URL: /docs/webhooks
Description: Receive real-time event notifications from PromoteKit

# Webhooks

PromoteKit sends webhook events to notify your application in real-time when things happen in your affiliate program. You can use webhooks to trigger automations, sync data to external systems, or build custom workflows.

## Setting Up Webhooks

1. Go to **Settings > Webhooks** in your PromoteKit dashboard
2. Click **Add Endpoint** to create a new webhook endpoint
3. Enter your endpoint URL (must be HTTPS)
4. Select which event types you want to receive
5. Save the endpoint

You can manage your endpoints, view delivery logs, and retry failed deliveries from the Webhooks settings page.

## Event Types

PromoteKit sends the following webhook events:

| Event                | Description                                                                      |
| -------------------- | -------------------------------------------------------------------------------- |
| `affiliate.created`  | A new affiliate is created (from the dashboard, API, or affiliate portal signup) |
| `affiliate.approved` | An affiliate is approved (individually or via bulk approval)                     |
| `referral.created`   | A new referral is tracked (from Stripe, the dashboard, API, or tracking script)  |
| `referral.converted` | A referral receives their first commission (first paid conversion)               |
| `commission.created` | A new commission is generated (from Stripe payments, the dashboard, or API)      |

## Payload Format

All webhook events are sent as HTTP POST requests with a JSON body. The payload has the following structure:

```json
{
  "type": "affiliate.created",
  "data": {
    // The resource object in the same format as the API
  }
}
```

The `data` field contains the resource object in the **same format as the corresponding API endpoint**. For example, an `affiliate.created` event contains the same affiliate object you would get from the [GET /affiliates/:id](/docs/api-reference/affiliates/get-affiliate) API endpoint.

### affiliate.created / affiliate.approved

```json
{
  "type": "affiliate.created",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "affiliate@example.com",
    "first_name": "Jane",
    "last_name": "Doe",
    "payout_email": "jane@example.com",
    "links": [
      {
        "url": "https://yoursite.com?via=jane",
        "code": "jane"
      }
    ],
    "promo_codes": [
      {
        "code": "JANE20",
        "external_id": "promo_abc123"
      }
    ],
    "clicks": 0,
    "approved": true,
    "banned": false,
    "details": null,
    "new_paid_referral_notifications": true,
    "created_at": "2025-01-15T10:30:00Z",
    "campaign": {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "name": "Default Campaign",
      "commission_type": "percentage",
      "commission_amount": 20
    }
  }
}
```

### referral.created / referral.converted

```json
{
  "type": "referral.created",
  "data": {
    "id": "770e8400-e29b-41d4-a716-446655440002",
    "email": "customer@example.com",
    "subscription_status": "signed_up",
    "signup_date": "2025-01-20T14:00:00Z",
    "stripe_customer_id": "cus_abc123",
    "created_at": "2025-01-20T14:00:00Z",
    "affiliate": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "email": "affiliate@example.com",
      "first_name": "Jane",
      "last_name": "Doe"
    }
  }
}
```

### commission.created

```json
{
  "type": "commission.created",
  "data": {
    "id": "880e8400-e29b-41d4-a716-446655440003",
    "revenue_amount": 99.0,
    "currency": "USD",
    "commission_amount": 19.8,
    "payout_status": "not_paid",
    "referral_date": "2025-01-20T14:00:00Z",
    "created_at": "2025-01-20T14:00:00Z",
    "stripe_payment_id": "pi_abc123",
    "affiliate": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "email": "affiliate@example.com",
      "first_name": "Jane",
      "last_name": "Doe"
    },
    "referral": {
      "id": "770e8400-e29b-41d4-a716-446655440002",
      "email": "customer@example.com",
      "subscription_status": "active",
      "signup_date": "2025-01-20T14:00:00Z",
      "stripe_customer_id": "cus_abc123",
      "created_at": "2025-01-20T14:00:00Z"
    },
    "payout": null
  }
}
```

## Verifying Webhook Signatures

PromoteKit signs all webhook payloads so you can verify they are authentic. Each webhook request includes the following headers:

| Header           | Description                                    |
| ---------------- | ---------------------------------------------- |
| `svix-id`        | Unique message identifier                      |
| `svix-timestamp` | Unix timestamp of when the message was created |
| `svix-signature` | The signature(s) for the payload               |

### Finding Your Signing Secret

1. Go to **Settings > Webhooks** in your dashboard
2. Click on your endpoint
3. Click **Signing Secret** to reveal your endpoint's secret

The secret starts with `whsec_`.

### Verifying with the Svix Library (Recommended)

Install the Svix library for your language:

<Tabs items={['JavaScript', 'Python']}>
<Tab value="JavaScript">
```bash
npm install svix
```
</Tab>
<Tab value="Python">
```bash
pip install svix
```
</Tab>
</Tabs>

Then verify the webhook:

<Tabs items={["JavaScript", "Python"]}>
<Tab value="JavaScript">
```javascript

const secret = "whsec_your_secret_here";
const wh = new Webhook(secret);

// In your webhook handler:
app.post("/webhook", (req, res) => {
  const headers = req.headers;
  const payload = req.body; // Must be the raw request body string

  try {
    const event = wh.verify(payload, headers);
    // Process the verified event
    console.log("Event type:", event.type);
    console.log("Event data:", event.data);
  } catch (err) {
    console.error("Webhook verification failed:", err);
    return res.status(400).send("Invalid signature");
  }

  res.status(200).send("OK");
});
```
</Tab>
<Tab value="Python">
```python
from svix.webhooks import Webhook

secret = "whsec_your_secret_here"
wh = Webhook(secret)

# In your webhook handler:
headers = request.headers
payload = request.body  # Must be the raw request body

try:
    event = wh.verify(payload, headers)
    # Process the verified event
    print("Event type:", event["type"])
except Exception as e:
    print("Webhook verification failed:", e)
    return Response(status=400)
```

</Tab>
</Tabs>

> **Warning:** You must use the **raw request body** when verifying webhooks. If your
  framework automatically parses JSON, you need to capture the raw body before
  parsing.

### Manual Verification

If you prefer to verify manually, the signature is computed as an HMAC-SHA256 of the message ID, timestamp, and body:

```
signed_content = "${svix_id}.${svix_timestamp}.${body}"
signature = base64(hmac_sha256(base64_decode(secret), signed_content))
```

Compare your computed signature against the one in the `svix-signature` header (after the `v1,` prefix).

## Retry Behavior

If your endpoint returns a non-2xx status code, PromoteKit will retry the delivery with exponential backoff. You can view delivery attempts and manually retry failed deliveries from the **Settings > Webhooks** page in your dashboard.

## Best Practices

- **Respond quickly**: Return a 2xx status code within 15 seconds. Process the event asynchronously if needed.
- **Handle duplicates**: Webhook deliveries may be retried. Use the `svix-id` header to deduplicate.
- **Verify signatures**: Always verify the webhook signature before processing events.
- **Use HTTPS**: Webhook endpoints must use HTTPS.