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

# Webhooks

> Receive real-time HTTP notifications when license events occur in your AuthForge app.

Webhooks send HTTP POST requests to your server when events happen on your licenses; creation, validation, revocation, and more. Use them to sync license state with your backend, trigger workflows, or update your database.

## How it works

1. You register a webhook URL in the dashboard or via the API.
2. When a matching event occurs, AuthForge sends an HTTP POST to your URL with a JSON payload.
3. Each request is signed with HMAC-SHA256 so you can verify it came from AuthForge.

```mermaid theme={null}
sequenceDiagram
    participant User as End User
    participant SDK as AuthForge SDK
    participant AF as AuthForge API
    participant Your as Your Server

    User->>SDK: login(licenseKey)
    SDK->>AF: POST /auth/validate
    AF-->>SDK: Success
    AF->>Your: POST webhook (license.validated)
    Your-->>AF: 200 OK
```

## Events

| Event                | Trigger                                  |
| -------------------- | ---------------------------------------- |
| `license.validated`  | Successful authentication via SDK        |
| `license.created`    | License key generated (dashboard or API) |
| `license.revoked`    | License revoked                          |
| `license.activated`  | Revoked license re-activated             |
| `license.hwid_bound` | HWID bound to a license slot             |
| `license.hwid_reset` | HWID bindings cleared                    |
| `license.deleted`    | License permanently deleted              |

<Note>
  `license.validated` fires on every successful SDK login. For high-traffic apps, consider subscribing only to the events you need.
</Note>

## Payload format

Every webhook delivery sends a JSON body like this:

```json theme={null}
{
  "event": "license.validated",
  "timestamp": "2026-04-10T15:30:00.000Z",
  "data": {
    "licenseKey": "A3K9-BFWX-7NP2-QHDT",
    "appId": "550e8400-e29b-41d4-a716-446655440000",
    "status": "active",
    "hwid": "a1b2c3d4e5f6..."
  }
}
```

### Headers

| Header                  | Description                                                         |
| ----------------------- | ------------------------------------------------------------------- |
| `Content-Type`          | `application/json`                                                  |
| `X-AuthForge-Event`     | The event name (e.g., `license.validated`)                          |
| `X-AuthForge-Timestamp` | ISO 8601 timestamp of the event                                     |
| `X-AuthForge-Signature` | `sha256=<hex>` where `<hex>` is HMAC-SHA256 of the raw request body |

## Signature verification

Every webhook is signed using the secret generated when you created the webhook. **Always verify the signature** before processing.

The signature is computed as:

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

### Verification example (Node.js / Express)

```javascript theme={null}
const crypto = require("crypto");

app.post("/webhooks/authforge", express.raw({ type: "application/json" }), (req, res) => {
  const signatureHeader = req.headers["x-authforge-signature"] || "";
  const timestamp = req.headers["x-authforge-timestamp"];
  const signature = signatureHeader.startsWith("sha256=")
    ? signatureHeader.slice("sha256=".length)
    : "";
  const expected = crypto
    .createHmac("sha256", process.env.AUTHFORGE_WEBHOOK_SECRET)
    .update(req.body)
    .digest("hex");

  const sigBuf = Buffer.from(signature, "hex");
  const expectedBuf = Buffer.from(expected, "hex");
  if (sigBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(sigBuf, expectedBuf)) {
    return res.status(401).send("Invalid signature");
  }
  if (!timestamp || Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60 * 1000) {
    return res.status(401).send("Stale timestamp");
  }

  const event = JSON.parse(req.body);
  console.log(`Received ${event.event} for ${event.data.licenseKey}`);

  // Handle the event
  switch (event.event) {
    case "license.validated":
      // Update last-seen timestamp in your DB
      break;
    case "license.revoked":
      // Suspend user access in your system
      break;
    case "license.created":
      // Send welcome email
      break;
  }

  res.sendStatus(200);
});
```

### Verification example (Python / Flask)

```python theme={null}
import hmac
import hashlib
from datetime import datetime, timezone
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ["AUTHFORGE_WEBHOOK_SECRET"]

@app.route("/webhooks/authforge", methods=["POST"])
def handle_webhook():
    signature_header = request.headers.get("X-AuthForge-Signature", "")
    timestamp = request.headers.get("X-AuthForge-Timestamp")
    signature = signature_header[7:] if signature_header.startswith("sha256=") else ""
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        request.data,
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(signature, expected):
        abort(401)
    if not timestamp:
        abort(401)
    if abs(datetime.now(timezone.utc).timestamp() - datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()) > 300:
        abort(401)

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

    # Handle the event...

    return "", 200
```

## Setup

### Via the dashboard

1. Go to your app's **Settings** → **Webhooks**
2. Click **Add Webhook**
3. Enter your HTTPS endpoint URL
4. Select which events to subscribe to (or select all)
5. Click **Create**
6. **Copy the webhook secret**: it's shown only once

### Via the Developer API

```bash theme={null}
curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/webhooks \
  -H "Authorization: Bearer af_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/authforge",
    "events": ["license.created", "license.revoked"],
    "enabled": true
  }'
```

The response includes a `secret` field; store it securely for signature verification.

## Limits

| Limit                | Value      |
| -------------------- | ---------- |
| Max webhooks per app | 5          |
| Delivery timeout     | 10 seconds |
| Retries              | None (v1)  |

## Testing

Use the test endpoint to send a sample payload to your webhook URL:

```bash theme={null}
curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/webhooks/WEBHOOK_ID/test \
  -H "Authorization: Bearer af_live_your_key"
```

This sends a signed `test.ping` event to verify your endpoint is receiving and verifying payloads correctly.

## Next steps

* [Webhooks API Reference](/api/webhooks); Full endpoint documentation
* [Commerce](/features/commerce); Stripe checkout into licenses; subscribe to `license.created` for your own fulfilment
