# Webhooks

Receive real-time events when AyeWatch detects changes in your monitored topics.

Source: https://ayewatch.ai/documentation/webhooks

## Setup

Configure your webhook endpoint URL in the [Developer dashboard → Webhook](https://ayewatch.ai/developer/webhook).
Your endpoint must:

- Be publicly accessible over **HTTPS**
- Respond with a `2xx` status within **10 seconds**
- Accept `POST` requests with a JSON body

> **Note:** Failed deliveries are retried with exponential backoff for up to 10 attempts. Make
> handlers idempotent because a delivery may be attempted more than once.

## When Webhooks Fire

AyeWatch sends a webhook only when new content is detected for a monitored topic. No request is sent
when a topic is checked but nothing has changed.

## Webhook Payload

AyeWatch sends a `POST` request with a JSON body to your endpoint:

```json
{
  "topic_id": 1042,
  "topic_name": "https://openai.com/news",
  "update_id": 7891,
  "headline": "GPT-5 announced",
  "content": "OpenAI today announced the release of GPT-5...",
  "content_detail": "...",
  "created_at": "2026-03-08T12:00:00Z"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `topic_id` | number | Integer ID of the monitored topic (matches the id from the Topics API). |
| `topic_name` | string | Name or URL of the topic as set on creation. |
| `update_id` | number | Integer ID of this specific update. Use it to deduplicate retried deliveries. |
| `headline` | string \| undefined | Short AI-generated summary of the detected change. |
| `content` | string \| undefined | Full content of the update. |
| `content_detail` | string \| undefined | Additional detail about the update. |
| `created_at` | string (ISO 8601) | Timestamp when the update was created. |
| `novelty_class` | string \| undefined | How new this is relative to what was already reported on the topic. |
| `score` | number \| undefined | Internal confidence in the update's significance. |
| `evidence` | array \| undefined | Supporting quotes with their source URLs: [{ quote, source_url }]. |
| `trigger` | string \| undefined | Which of the topic's triggers fired, when the update came from one. |

## Signature Verification

Every webhook request includes an `X-AyeWatch-Signature` header containing an HMAC-SHA256 signature
signed with your webhook secret. Verify against the raw request bytes — not a re-serialized version
of the parsed payload.

Header format: `X-AyeWatch-Signature: sha256=<hex_digest>`

Always verify this signature before processing webhook events. Reject events that fail verification.

Node.js / Express:

```js
const crypto = require("crypto");

function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = "sha256=" + crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  // Use timingSafeEqual to prevent timing attacks
  const expectedBuffer = Buffer.from(expected, "utf8");
  const signatureBuffer = Buffer.from(signature, "utf8");

  if (expectedBuffer.length !== signatureBuffer.length) return false;
  return crypto.timingSafeEqual(expectedBuffer, signatureBuffer);
}

// Express example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-ayewatch-signature"];
  const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);

  if (!isValid) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  const event = JSON.parse(req.body);
  console.log("Update received for topic:", event.topic_id, event.headline);
  res.status(200).json({ received: true });
});
```

Python / FastAPI:

```python
import hashlib
import hmac

def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

# FastAPI example
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/webhook")
async def webhook(request: Request):
    raw_body = await request.body()
    signature = request.headers.get("x-ayewatch-signature", "")

    if not verify_webhook_signature(raw_body, signature, WEBHOOK_SECRET):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = await request.json()
    print("Update received for topic:", event["topic_id"], event.get("headline"))
    return {"received": True}
```

## Delivery Behavior

- **Retries:** Non-2xx responses and timeouts are retried with exponential backoff, up to 10
  attempts.
- **10-second timeout:** Requests that don't complete within 10 seconds are abandoned.
- **HTTPS only:** Plain HTTP endpoints are not supported.
- **Order not guaranteed:** Events may arrive out of order under load.
- **Additive payloads:** New fields may be added over time. Ignore any you don't recognise rather
  than rejecting the request.

## Rotating Your Secret

You can rotate the signing secret from the [Developer dashboard → Webhook](https://ayewatch.ai/developer/webhook).
Rotation takes effect immediately — there is no overlap window, so deliveries signed with the new
secret will fail verification until your receiver is updated. Deploy the new secret first, or rotate
during a window where a few retried deliveries are acceptable.
