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

# Explore webhook event schemas from the catalog

> Answer 'what events exist, and what does each payload look like' before wiring a receiver — browse the dashboard event catalog, fetch the schema over the API, validate with JSON Schema, and pin a fingerprint in CI to catch drift.

# Explore webhook event schemas from the catalog

## What the catalog answers

Every webhook endpoint you register subscribes to named event types — `message.delivered`, `message.failed`, `call.completed`, and the rest of the catalog. The question you need answered before any receiver code runs is two-fold: **which events exist**, and **what shape is each payload**. Until now answering it meant waiting for the first delivery to land, or hand-mining the reference pages.

The event schema catalog closes that gap. Orbit publishes one machine-readable entry per event type, each carrying:

* `event_type` — the literal you subscribe to (e.g. `message.delivered`).
* `json_schema` — a JSON Schema Draft-07 envelope schema with `required: ["id", "type", "created_at", "data"]` and the `type` literal pinned via `const`, so a validator rejects a mismatched envelope.
* `example_payload` — a realistic example of the full envelope, so you can see exactly what your endpoint receives.
* `fingerprint` — a deterministic `sha256:<16 hex>` content hash of the schema. Pin it in CI and any schema change — a new required field, a tightened enum — breaks the build loudly instead of your receiver parsing nulls at runtime.

Wiring a receiver without the catalog means one of two failure modes: your code guesses the field set (wrong), or it treats `data` as an opaque bag (unvalidated). Either way the defect ships silently. Read the schema first.

<Note>
  The catalog is static platform metadata — the same for every tenant — and lives behind the same `webhooks:read` scope as the rest of the webhooks API.
</Note>

## Browse in the dashboard

Open **Developer → Webhooks → Event catalog** in the dashboard. The page lists every event type Orbit can dispatch with a search box that matches on the event-type string and the example payload; pick an event and the detail pane shows its copyable fingerprint chip and the pretty-printed example payload. This is the same catalog the API endpoint serves, rendered for browsing.

Use the dashboard when you are picking which events to subscribe to. Use the API (next section) when you are generating types or wiring CI.

## Fetch over the API

`GET /api/v1/webhooks/event-schemas` returns the full catalog. Every entry sits in the standard `{ data, meta }` envelope, so the array of schema entries lives at `data` in the response.

```bash theme={null}
curl -s https://api.orbit.devotel.io/api/v1/webhooks/event-schemas \
  -H "X-API-Key: dv_live_sk_..." \
| jq '.data[] | {event_type, fingerprint}'
```

Narrow it down to one surface with `jq` — here everything in the `message` family:

```bash theme={null}
curl -s https://api.orbit.devotel.io/api/v1/webhooks/event-schemas \
  -H "X-API-Key: dv_live_sk_..." \
| jq '.data[] | select(.event_type | startswith("message"))'
```

Pick your `event_type` out of that list and pull the whole entry, including the realistic example:

```bash theme={null}
curl -s https://api.orbit.devotel.io/api/v1/webhooks/event-schemas \
  -H "X-API-Key: dv_live_sk_..." \
| jq '.data[] | select(.event_type == "message.delivered")'
```

```json theme={null}
{
  "event_type": "message.delivered",
  "json_schema": {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "WebhookEvent<message.delivered>",
    "type": "object",
    "required": ["id", "type", "created_at", "data"],
    "properties": {
      "id": { "type": "string" },
      "type": { "type": "string", "const": "message.delivered" },
      "created_at": { "type": "string", "format": "date-time" },
      "data": { "type": "object", "required": ["message_id", "channel", "status"], "...": "..." }
    },
    "additionalProperties": false
  },
  "example_payload": {
    "id": "evt_3f1c8b2a9d4e5f7a8b6c1d2e3f4a5b6c",
    "type": "message.delivered",
    "created_at": "2026-05-28T12:00:00Z",
    "data": {
      "message_id": "msg_xyz789",
      "channel": "sms",
      "to": "+1415555****",
      "from": "+1415000****",
      "status": "delivered",
      "delivered_at": "2026-05-28T12:00:00Z"
    }
  },
  "fingerprint": "sha256:4f2a1c8e9b3d7f0a"
}
```

The example is the payload shape as it actually arrives — maskable phone numbers are prefix-masked (`+1415555****`), the envelope is flat `{ id, type, created_at, data }`, and there is no wrapper you need to unwrap. Copy the `example_payload` into a fixture and you have a test body that round-trips the schema.

## Validate your receiver against the schema

Each entry's `json_schema` is a complete Draft-07 document. Feed it to any Draft-07-capable validator and your receiver stops trusting wire bytes blind.

### Node.js with `ajv`

```bash theme={null}
npm install ajv
```

```javascript theme={null}
import Ajv from "ajv";
import addFormats from "ajv-formats";

const ajv = new Ajv({ strict: true });
addFormats(ajv); // enables the `format: "date-time"` checks the envelope uses

// The entry as returned by GET /api/v1/webhooks/event-schemas, per event.
const entry = await fetchEventSchema("message.delivered");
const validate = ajv.compile(entry.json_schema);

export function handleDelivery(payload) {
  if (!validate(payload)) {
    // Fail closed — a mismatched envelope is a wire-protocol bug, not a
    // business-logic failure. Log validate.errors and drop the event.
    throw new SchemaMismatch(entry.event_type, validate.errors);
  }
  return routeEvent(payload.type, payload.data);
}

async function fetchEventSchema(eventType) {
  const res = await fetch(
    "https://api.orbit.devotel.io/api/v1/webhooks/event-schemas",
    { headers: { "X-API-Key": process.env.ORBIT_API_KEY } },
  );
  const { data } = await res.json();
  return data.find((e) => e.event_type === eventType);
}
```

Validation runs **after** signature verification — a payload you have not authenticated is wire noise, and validating it burns cycles on attacker bytes. Do it in the queue worker, past signature + dedupe checks.

### Generate a TypeScript model instead of runtime-checking

If you prefer compile-time types, hand the schema to a codegen tool (`json-schema-to-typescript`, `quicktype`) and emit a model:

```bash theme={null}
# Fetch once at codegen time, then generate.
curl -s https://api.orbit.devotel.io/api/v1/webhooks/event-schemas \
  -H "X-API-Key: $ORBIT_API_KEY" \
| jq '.data[] | select(.event_type == "message.delivered") | .json_schema' \
    > schema.message.delivered.json

npx json-schema-to-typescript schema.message.delivered.json \
  > types/message-delivered.d.ts
```

Regenerate in CI on a schedule and let the diff review catch schema moves. Pair that with the fingerprint pin below so the schema move also fails the build instead of sneaking through.

## Pin the fingerprint in CI

Every entry carries `fingerprint` — `sha256:<16 hex>` over the canonical schema content. Because it depends on schema content only (never key ordering), a subscriber can pin it, and any change to the schema breaks the build at the pin instead of corrupting a live receiver.

Save this as `scripts/check-webhook-schema-fingerprint.mjs` and run it in CI:

```javascript theme={null}
#!/usr/bin/env node
// Pin the webhook-event-schema fingerprint. Fail the build the moment the
// published schema drifts from the version your consumer was written against.
import assert from "node:assert/strict";

const API_BASE = "https://api.orbit.devotel.io/api/v1";
const PINS = {
  "message.delivered": "sha256:4f2a1c8e9b3d7f0a",
  "message.failed": "sha256:9e1d3b7c2a4f6081",
  "call.completed": "sha256:7b3e9a1f4c2d5086",
};

const res = await fetch(`${API_BASE}/webhooks/event-schemas`, {
  headers: { "X-API-Key": process.env.ORBIT_API_KEY },
});
assert.ok(res.ok, `event-schemas fetch failed: ${res.status}`);
const { data } = await res.json();

for (const [eventType, expected] of Object.entries(PINS)) {
  const entry = data.find((e) => e.event_type === eventType);
  assert.ok(entry, `event type ${eventType} is missing from the catalog`);
  assert.equal(
    entry.fingerprint,
    expected,
    `Schema drift on ${eventType}: expected ${expected}, got ${entry.fingerprint}. ` +
      "Review the diff before re-pinning — a changed fingerprint means a changed payload contract.",
  );
}
```

A drift failure is the point — it forces a human to review the schema diff and re-pin intentionally. Update the pinned value after the review, never by reflex.

In your consumer, treat the fingerprint as a schema-version handle too. When a payload validates against a schema whose fingerprint is in `PINS`, you are on a known contract; log the fingerprint alongside the event id to build an audit trail of which schema version processed which events.

## Next steps

* [Event payloads](/webhooks/event-payloads) — the reference render of the same catalog, one event at a time
* [Build a durable webhook consumer](/guides/webhook-consumer) — the full receiver this catalog feeds
* [Webhook security](/webhooks/security) — signature verification to pair with schema validation
