# Webhooks

> Receive signed HTTPS notifications when a budget is crossed, an invoice is issued, a request is refused by policy or a key is revoked.

Product: AI Cost Optimizer Gateway (ACOG). Web version: https://tessarac.com/docs/acog/administer/webhooks. Last updated: 2026-10-16.

Webhooks tell your systems when something happens in your organization. Every delivery is signed, retried on failure and recorded in a delivery log.

Open **Admin → Webhooks**. You need the **Edit the organization's profile** permission (`admin:org`).

## Adding an endpoint

Choose **Add endpoint** and enter:

- an HTTPS URL. On the hosted service it must be a public address on port 443; a self-hosted installation can deliver to any port;
- a description;
- the events to send.

The endpoint's **signing secret is shown once**. Store it with your receiver. An organization can have up to 20 endpoints.

## Events

| Event | When |
|---|---|
| `budget.threshold_crossed` | A budget first passes 50, 80 or 90% of its limit in a period |
| `budget.exhausted` | A budget reaches 100% of its limit; requests against it are refused |
| `billing.invoice_created` | A monthly invoice is issued |
| `billing.invoice_paid` | An invoice is paid |
| `billing.payment_failed` | A payment attempt fails |
| `billing.state_changed` | Savings switch between active and pass-through |
| `governance.guardrail_blocked` | Prompt injection screening blocks a prompt |
| `governance.dlp_blocked` | Data loss prevention blocks or masks a request |
| `governance.model_access_denied` | A caller asks for a model it may not use |
| `governance.rate_limit_exceeded` | A caller exceeds a rate limit |
| `security.api_key_created` | A key is created or rotated in |
| `security.api_key_revoked` | A key is revoked, deleted, or removed with its member |
| `security.member_invited` | Someone is invited (hosted) or added (self-hosted) |
| `security.member_removed` | A member is removed |
| `security.sso_connection_changed` | A single sign-on connection is created, changed or deleted |
| `webhook.ping` | **Send test** on an endpoint |

The four `governance.*` events are sent at most once a minute for each caller and rule; every refusal is still on the Activity page. **No event contains prompt text, a detected value or a secret.**

## What your receiver gets

One `POST` per event and endpoint:

```http
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: acog-webhooks/1
Acog-Event: budget.threshold_crossed
Acog-Delivery: whd_8Kd2…
Acog-Signature: t=1792161166,v1=5f0c3a…

{"id": "whev_3xQ…", "type": "budget.threshold_crossed", "created_at": "2026-10-16T14:32:18Z",
 "data": {"scope": "org", "pool_id": "…", "window": "month", "pct": 80, "used_usd": 4000, "limit_usd": 5000}}
```

`id` identifies the event and is the same on every attempt and every endpoint. `Acog-Delivery` identifies this event's delivery to this endpoint and is the same on every retry: use either to ignore duplicates.

## Verifying the signature

`Acog-Signature` carries a timestamp `t` and `v1`, the hex HMAC-SHA256 of `"<t>.<raw body>"` keyed with the endpoint's secret.

```python
import hashlib, hmac, time

def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    parts = dict(item.split("=", 1) for item in header.split(","))
    timestamp, signature = parts["t"], parts["v1"]
    if abs(time.time() - int(timestamp)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature)
```

Verify against the raw body exactly as received, compare in constant time, and reject a timestamp more than five minutes from now.

## Responding and retries

Answer with any `2xx` status within 10 seconds, then do the work. Anything else is a failure, including a redirect (redirects are not followed).

- A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours.
- After 50 consecutive failures the endpoint is switched off; the console says why.
- A delivery still undelivered after 3 days is given up.
- Delivery is at least once and events are not ordered.

Events that happen while an endpoint is switched off are not queued for it.

## Managing an endpoint

Open an endpoint to see its delivery log (every attempt and its payload, kept 30 days), change its events, switch it off or on, **rotate its secret** (the old secret stops working immediately) or **send a test**.
