---
title: Events and Webhooks
path: core-concepts/events-and-webhooks
status: published
---

# Events and Webhooks

ScaiGrid emits events. You can subscribe to them via outbound webhooks — ScaiGrid posts to your HTTP endpoint when something interesting happens.

## Event model

Events have a type, a source module, a tenant/partner scope, and a payload. Example:

```json
{
  "event_type": "request.completed",
  "event_id": "evt_abc123",
  "source": "inference_service",
  "tenant_id": "tenant_acme",
  "partner_id": "partner_internal",
  "payload": {
    "model": "scailabs/poolnoodle-omni",
    "backend_id": "be_openai_gpt4o",
    "latency_ms": 842,
    "tokens": 156
  },
  "timestamp": "2026-04-22T14:30:00.000Z"
}
```

Events are delivered at-least-once. Your webhook handler should be idempotent — check the `event_id` before acting on an event you may have already processed.

## Core event types

| Event type | When emitted |
|------------|--------------|
| `request.completed` | Every successful inference call |
| `request.failed` | Every failed inference call |
| `budget.soft_limit_reached` | Spend crossed `soft_limit_pct` for a budget |
| `budget.hard_limit_reached` | Spend crossed the hard limit, requests now blocked |
| `scaikey.user.created` | New user provisioned via ScaiKey webhook |
| `scaikey.user.updated` | User roles or profile changed |

## Module event types

Modules emit their own events. Full list per module:

- **ScaiCore** — `scaicore.activated`, `scaicore.passivated`, `scaicore.deleted`, `scaicore.checkpoint_pending`, `scaicore.checkpoint_resolved`
- **ScaiQueue** — `scaiqueue.message.published`, `scaiqueue.message.claimed`, `scaiqueue.message.completed`, `scaiqueue.message.dead_lettered`
- **ScaiBunker** — `scaibunker.usage` (per-60s tick), `swp.bunker.*` (conversation-linked bunker events)
- **ScaiBot** — `scaibot.conversation.started`, `scaibot.escalation.triggered`
- **ScaiMatrix** — `scaimatrix.document.indexed`, `scaimatrix.crawl.completed`

Each module's documentation lists its events.

## Outbound webhooks

Register a webhook to receive events:

```bash
curl -X POST https://scaigrid.scailabs.ai/v1/webhooks \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-service.example/scaigrid-webhooks",
    "events": ["request.completed", "request.failed", "budget.soft_limit_reached"],
    "secret": "whsec_random_string_for_signing"
  }'
```

ScaiGrid POSTs each matching event to your URL as JSON:

```http
POST /scaigrid-webhooks HTTP/1.1
Host: your-service.example
Content-Type: application/json
X-ScaiGrid-Signature: sha256=...
X-ScaiGrid-Event: request.completed
X-ScaiGrid-Delivery: del_xyz789

{
  "event_type": "request.completed",
  "event_id": "evt_abc123",
  ...
}
```

Respond with `2xx` within 10 seconds. Any other response (including timeouts) is a failed delivery.

## Signature verification

Each webhook delivery is signed with HMAC-SHA256 using your `secret`. Verify before trusting:

```python
import hmac, hashlib

def verify_webhook(payload: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)
```

```typescript
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(payload: Buffer, signatureHeader: string, secret: string): boolean {
  const expected = "sha256=" + createHmac("sha256", secret).update(payload).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
```

Always verify before parsing the body. An unverified webhook could be an attacker.

## Retry behavior

Failed deliveries retry with exponential backoff: 30 seconds, 2 minutes, 10 minutes, 30 minutes, 2 hours. After 5 consecutive failures on the same webhook, it's marked `failing` and stops receiving events until you re-enable it. After 50 consecutive failures across any events, the webhook is auto-disabled entirely.

Per-webhook delivery history:

```bash
curl "https://scaigrid.scailabs.ai/v1/webhooks/{webhook_id}/deliveries" \
  -H "Authorization: Bearer $TOKEN"
```

Useful when a webhook is failing and you need to see why.

## Inbound webhooks

ScaiGrid also has an inbound webhook path at `/v1/internal/scaikey/webhook` for ScaiKey to push user/tenant lifecycle events. This is configured server-side during setup and isn't something you integrate with as an application developer.

## Replay

If your webhook endpoint was down and you missed events, replay via the admin UI or:

```bash
curl -X POST "https://scaigrid.scailabs.ai/v1/webhooks/{webhook_id}/replay?since=2026-04-22T00:00:00Z" \
  -H "Authorization: Bearer $TOKEN"
```

## Event bus internals

Under the hood, events flow through Redis Streams. Modules publish events; core workers forward them to webhooks. The stream retains events for a configurable window (default 100K entries) so you can replay recent events without touching the database.

Advanced users can consume directly from Redis via gRPC — see [gRPC API](../07-advanced/01-grpc-api.md).

## What's next

- [Webhooks Deep Dive](../07-advanced/03-webhooks-deep-dive.md) — retry tuning, per-webhook transforms.
- [Webhooks Reference](../06-reference/08-webhooks.md) — full endpoint list.
