# Webhooks

Verify signatures, parse events, and handle Trueyy's delivery guarantees.

Trueyy delivers events to your backend as they happen. The SDK ships Express-compatible middleware that validates the signature, rejects replays, and parses the event for you.

## `webhooks.verify(secret)`

Returns Express middleware. It validates `X-Trueyy-Signature` (HMAC-SHA256 over the raw body), rejects replays (timestamp older than 5 minutes), parses the body, and attaches `req.trueyy.event`.

<Callout type="warn" title="Mount after express.raw()">
The HMAC is computed over the **raw request bytes**. Mount `express.raw({ type: "application/json" })` **before** the verify middleware, or signature validation will fail.
</Callout>

```ts
import express from "express";

app.post(
  "/webhooks/trueyy",
  express.raw({ type: "application/json" }),   // MUST come first - raw bytes for HMAC
  trueyy.webhooks.verify(WEBHOOK_SECRET),
  async (req, res) => {
    const event = req.trueyy!.event;
    // event = { event_id, event_type, created_at, api_version, data }

    // 1. Idempotency check on event_id
    if (await alreadySeen(event.event_id)) return res.sendStatus(200);

    // 2. Dispatch by event_type
    switch (event.event_type) {
      case "session.ended":
        await persistFinalReport(event.data);
        break;
      case "session.risk_pulse":
        await alertsPipeline.push(event.data);
        break;
      // ...
    }

    // 3. Acknowledge - anything non-2xx triggers Trueyy's retry chain.
    res.sendStatus(200);
  }
);
```

The middleware responds:

- `401` - missing/invalid signature, or missing `x-trueyy-event-id` / `x-trueyy-event-type` headers.
- `400` - raw body unavailable or not JSON.

## Event catalog

Every event shares the envelope `{ event_id, event_type, created_at, api_version, data }`.

| `event_type` | Fires when | Sample `data` |
|---|---|---|
| `session.ready` | Candidate + helper online and ready | `{ session_id, ... }` |
| `session.transcript_segment` | A final transcript fragment is produced | `{ session_id, speaker, text, started_at, ended_at }` |
| `session.risk_pulse` | A risk alert fires (AI tool detected, paste threshold) | `{ session_id, kind, severity, evidence }` |
| `session.window_result` | A 30-second analysis window completes | `{ session_id, window_id, risk, score, summary, modality_breakdown }` |
| `session.image_analysis_result` | Vision LLM completes screenshot analysis | `{ session_id, image_id, risk, findings }` |
| `session.ended` | A round moves to ENDED (any path) | `{ session_id, ... }` |
| `session.report_ready` | The round's analysis report is generated | `{ session_id, ... }` |
| `session.cancelled` | Cancelled before start | `{ session_id, ... }` |

## Delivery guarantees

<Cards>
  <Card title="At-least-once" description="Delivered at least once within ~15 hours. Make your side exactly-once by idempotency-checking event_id." />
  <Card title="Retry schedule" description="1m → 5m → 30m → 2h → 12h, then dead-lettered." />
  <Card title="Auto-disable" description="After 50 consecutive failures your endpoint auto-disables and we email the admin." />
  <Card title="Re-fire" description="Replay dead-lettered events from the dashboard - the original event_id is preserved." />
</Cards>
