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

# Webhooks

> Get a signed HTTP push the moment Glassray confirms a deviation - so your own systems act on it without polling.

When Glassray detects a deviation, it can **push a signed webhook to a URL you control**. Your systems act on it the moment we find the problem - no polling - and you run whatever loop you like on your side: Glassray detects and emits, your harness decides and acts, and you write the outcome back through the [API / MCP](/mcp-server).

Delivery is handled by [Svix](https://www.svix.com), so each request is signed, retried on failure, and inspectable in a delivery dashboard.

## Register an endpoint

From **Settings → Webhooks**:

1. Paste your endpoint URL (must be `https://` and publicly reachable).
2. Choose which events fire - **Confirmed** by default.
3. Save. Glassray shows the **signing secret once** (starts with `whsec_`) - store it now; you use it to verify our calls.

You can register multiple endpoints, toggle them on/off, rotate the secret, send a test event, and view recent deliveries - all from the same screen.

## When it fires

A webhook fires when a deviation reaches an actionable point in its [lifecycle](/deviations#lifecycle). Each event has a `type`:

| `type`                | Fires when                                                      | Enabled by default |
| --------------------- | --------------------------------------------------------------- | ------------------ |
| `deviation.confirmed` | A deviation is promoted to **Confirmed** (real, recurring work) | ✅                  |
| `deviation.resolved`  | A deviation is marked **Done**                                  | -                  |
| `deviation.dismissed` | A deviation is **Cancelled** (by-design / not a bug)            | -                  |

<Note>
  We recommend keying your logic on the **`type`** field. The `status` inside the payload uses Glassray's internal values - `triage` (Suspected), `todo` (Confirmed), `done`, `cancelled`.
</Note>

## Payload

Each event is **thin but sufficient** - enough to triage and act without a callback, plus deep links to fetch more. `id` is a stable event id you can use to **dedupe**.

```json theme={null}
{
  "id": "devt_01KWKTQNZ5HJJPVQYAMVB47CY1",
  "type": "deviation.confirmed",
  "createdAt": "2026-07-03T11:10:52.923Z",
  "organizationId": "org_01KSQVRADMBQPM34P56CS7ZSR4",
  "data": {
    "deviation": {
      "id": "dev_01KWHKTARJ9QGWYHM29N0B6REM",
      "label": "Miscalibrated confidence / QA gate",
      "description": "The QA gate reports 'high' confidence even when the answer is weakly grounded.",
      "severity": "major",
      "status": "todo",
      "statusReason": "Promoted to todo - confirmed a real defect.",
      "flow": { "id": "flow_01…", "name": "Answer question" },
      "rule": "The confidence gate should report a pass only when the answer is grounded in the retrieved sources.",
      "url": "https://app.glassray.ai/deviations/d/dev_01KWHKTARJ9QGWYHM29N0B6REM",
      "createdAt": "2026-07-02T14:30:39.125Z",
      "statusChangedAt": "2026-07-03T11:10:01.179Z"
    },
    "transition": { "from": "triage", "to": "todo", "reason": "Promoted to todo - confirmed a real defect." },
    "evidence": [
      {
        "id": "dinst_01KWHKTASQWQYH0QQD1S1EC7ME",
        "traceId": "trc_01KWGVB0XG2M3B7NYE7AHXKRD4",
        "severity": "major",
        "description": "The confidence scorer rated the answer 'high' though it's grounded on an off-topic source.",
        "evidence": "Step 98 output: {\"score\":\"high\"} for a response citing an unrelated source.",
        "glassrayUrl": "https://app.glassray.ai/deviations/d/dev_01KWHKTARJ9QGWYHM29N0B6REM",
        "providerUrl": "https://eu.smith.langchain.com/o/…/r/…",
        "divergence": [
          {
            "step": "Rate answer confidence",
            "agent": "confidenceScore",
            "expected": "Calibrate confidence to source support and coherence",
            "actual": "Rated 'high' despite off-topic source and contradiction"
          }
        ],
        "occurredAt": "2026-04-24T09:11:48.023Z"
      }
    ]
  }
}
```

### Fields

| Field             | Meaning                                                                                                                                                                                    |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `id`              | Stable event id (`devt_…`) - use it as your **idempotency / dedupe key**.                                                                                                                  |
| `type`            | The event that fired (see the table above).                                                                                                                                                |
| `data.deviation`  | Identity, `severity`, `status` + `statusReason`, the `flow` it belongs to, the `rule` it violated (the "Expected"), and a `url` deep link into Glassray.                                   |
| `data.transition` | The status change (`from` → `to`) and its recorded reason. `from` is `null` for a human/automatic confirm.                                                                                 |
| `data.evidence[]` | The triggering trace(s): a `description`, an `evidence` quote, `glassrayUrl` + `providerUrl` (Langfuse / LangSmith) deep links, and the `divergence` - each step's `expected` vs `actual`. |

## Verifying signatures

Every request carries three headers you should verify so you know the call is really from Glassray:

```
svix-id:         msg_2b…                    (unique message id)
svix-timestamp:  1783077053                 (unix seconds - bounds replay)
svix-signature:  v1,g0Xb…                   (HMAC-SHA256 signature)
```

Verify with the official [`svix`](https://docs.svix.com/receiving/verifying-payloads/how) library - **pass the raw request body**, not parsed JSON:

<CodeGroup>
  ```ts Node theme={null}
  import { Webhook } from "svix";

  const wh = new Webhook(process.env.GLASSRAY_WEBHOOK_SECRET!); // whsec_…

  app.post("/glassray/webhook", (req, res) => {
    let event;
    try {
      event = wh.verify(req.rawBody, {
        "svix-id": req.header("svix-id")!,
        "svix-timestamp": req.header("svix-timestamp")!,
        "svix-signature": req.header("svix-signature")!,
      });
    } catch {
      return res.sendStatus(401); // bad signature
    }

    res.sendStatus(200);          // ack fast - do work async
    // dedupe on event.id, then hand `event` to your harness
  });
  ```

  ```python Python theme={null}
  from svix.webhooks import Webhook, WebhookVerificationError

  wh = Webhook(os.environ["GLASSRAY_WEBHOOK_SECRET"])  # whsec_…

  @app.post("/glassray/webhook")
  async def receive(request: Request):
      try:
          event = wh.verify(await request.body(), {
              "svix-id": request.headers["svix-id"],
              "svix-timestamp": request.headers["svix-timestamp"],
              "svix-signature": request.headers["svix-signature"],
          })
      except WebhookVerificationError:
          return Response(status_code=401)
      # ack fast, dedupe on event["id"], then act
      return Response(status_code=200)
  ```
</CodeGroup>

<Note>
  Verifying is a security control, not a requirement - the delivery works without it. But since the event can trigger automated action on your side, we strongly recommend it: the endpoint is public, so verification is how you know a call really came from Glassray.
</Note>

## Delivery guarantees

* **At-least-once.** A confirmed deviation is delivered even across transient failures, so **dedupe on the event `id`** - the same event may arrive more than once.
* **Retries with backoff.** A non-`2xx` response (or a timeout) is retried automatically. **Return `2xx` quickly** and do your work asynchronously.
* **Inspectable.** Every attempt - status, response code, retries - is visible under **View deliveries** in settings.

## Closing the loop

The webhook is the missing **push**. Once your harness acts on an event, write the outcome back through the surfaces that already exist - mark the deviation **Done** or **not-a-bug**, or add a comment - via the [MCP server](/mcp-server) (`update_deviation_status`, `get_deviation`, `list_deviations`).

<Card title="MCP tools" icon="plug" href="/mcp-server">
  Read and update deviations programmatically - the write-back half of the loop.
</Card>
