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

# Webhooks

> Accept a ticket with 202, then verify the signed completion POST.

Two directions. You POST a ticket in. judged.systems POSTs a completion out, after the judgment is stored.

## Inbound

`POST /api/webhooks/inbound` accepts an API key with `judgments:run`. The body is the same `{ packId, ticket }` as [Judge a ticket](/docs/guides/rest). The pack must be published.

`Idempotency-Key` is required. It is the delivery id. The same key returns the existing event and does not start a second judgment.

The response is `202`:

```json theme={null}
{ "eventId": "cm7event" }
```

Judging continues after that response. Read the evaluation when the completion arrives, or poll `GET /api/evaluations`.

### Optional signature

`X-Judged-Signature` is optional on the inbound request. When you send it, it is checked against the saved completion secret: `sha256=` plus HMAC-SHA256 of the raw body. A mismatch, or a signature with no saved endpoint, is `401` with `invalid signature`. Omit the header to skip the check.

## Outbound completion

Each user has one HTTPS endpoint. Save it with `packs:write`:

```bash theme={null}
curl https://judged.systems/api/webhook-endpoints \
  -X POST \
  -H "x-api-key: $JUDGED_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "url": "https://example.com/judged/completion" }'
```

`201` returns `{ id, url, secret }`. The secret is shown once. Saving again replaces the endpoint and returns a new secret. `GET /api/webhook-endpoints` lists `{ id, url }` and does not include the secret. `DELETE /api/webhook-endpoints/{id}` returns `204`.

The URL is public `https` with no userinfo.

### The POST you receive

The body is the evaluation id and the result. It has no ticket.

```json theme={null}
{
  "evaluationId": "cm7evaluation",
  "result": {
    "status": "review",
    "answers": {},
    "reviewReasons": ["urgency: score is in the review range"],
    "modelId": "typesafe-ai/jev",
    "latencyMs": 840
  }
}
```

Header `X-Judged-Signature` is required. Value: `sha256=` plus HMAC-SHA256 of the raw body. Compare with a constant-time check against the bytes you received, before you parse JSON.

```javascript theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

const expected = Buffer.from(
  "sha256=" + createHmac("sha256", process.env.JUDGED_WEBHOOK_SECRET).update(rawBody).digest("hex"),
);
const received = Buffer.from(signatureHeader);
const valid = expected.length === received.length && timingSafeEqual(expected, received);
```

Respond with any `2xx`. Redirects are not followed.

| Your response | What happens |
| - | - |
| `2xx` | Delivered |
| Network failure, `429`, or `5xx` | Retried, up to 5 attempts |
| Any other status | Not retried |

`GET /api/webhook-deliveries` returns up to 50 rows, newest first: `status` is `pending`, `retrying`, `delivered`, or `failed`, plus `attemptCount`, `lastStatusCode`, and `lastError`.

In the app, the delivery log labels the event `evaluation.completed`. That name is not a field on the body.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.