# Vellum Webhooks

Vellum, the LLM app platform, can stream platform events (workflow executions, usage, metrics)
to your endpoint through the organization **Monitoring Integration > Webhook Integration**.
Requests are **unsigned**: you authenticate them with a secret header you add yourself, using
Vellum's **API Key Header** or **Bearer Token** authentication option.

Vellum can also sign requests with HMAC (`X-Vellum-Timestamp` and `X-Vellum-Signature`, over the
timestamp, method, URL and body). Hookdeck's Vellum source does not check that signature, and
this skill does not verify it either.

## When to Use This Skill

* How do I receive Vellum webhooks?
* How do I authenticate Vellum monitoring webhooks (API Key Header or Bearer Token)?
* Does Vellum sign webhook payloads? (Optionally, but this skill uses the shared header instead)
* How do I handle `workflow.execution.rejected` or `metric.execution.fulfilled` events?
* Why does my Vellum webhook endpoint return 401?

## Setup note (Hookdeck source)

Hookdeck's Vellum source (type `VELLUM`, labelled "Vellum") shows this note, and it applies to
your own endpoint too:

> Turn on authentication and choose API Key Header (or Bearer Token, with header name Authorization and value "Bearer <token>"). The HMAC signature option is not supported.

The source pre-selects the POST method without managing it. Vellum's docs don't name the HTTP
method for webhooks, so if your sender uses another method, allow it on the source as well. The
examples accept GET, POST, PUT, PATCH and DELETE.

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyVellumRequest(
  headers,
  secret = process.env.VELLUM_WEBHOOK_SECRET,
  headerName = process.env.VELLUM_WEBHOOK_HEADER || 'Authorization'
) {
  if (!secret) return false;                          // never accept when unconfigured
  const received = headers[headerName.toLowerCase()]; // Node lowercases header names
  return typeof received === 'string' && received !== '' && safeEqual(received, secret);
}

```

* The header name is whatever you chose (`VELLUM_WEBHOOK_HEADER`, default `Authorization`). The
  expected value (`VELLUM_WEBHOOK_SECRET`) is the **exact, whole header value**, including any
  `Bearer ` prefix you typed. Look the header up case-insensitively and compare the whole value;
  don't strip or parse `Bearer `.
* Missing header or mismatch: `401`. Check it first, for every HTTP method.
* Python: `hmac.compare_digest` on **bytes** (encode both sides; `str` raises on non-ASCII).
* Read the body raw (`express.raw({ type: '*/*' })`) and parse it only after the check. Handle an
  empty body.
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vellum-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vellum-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vellum-webhooks/examples/fastapi/).

## Event Types

The payload is `{id, timestamp, api_version, trace_id, span_id, parent, name, body}`. Dispatch on
`name` and dedupe on `id`.

| `name` | Sent When |
| --- | --- |
| `workflow.execution.initiated` | A workflow starts execution |
| `workflow.execution.fulfilled` | A workflow completes successfully |
| `workflow.execution.rejected` | A workflow execution fails |
| `workflow_execution.usage_calculation.fulfilled` | Workflow usage is calculated |
| `workflow_execution.usage_calculation.rejected` | Workflow usage calculation fails |
| `metric.execution.fulfilled` | A metric execution completes successfully |

Vellum's schema example spells the usage event `workflow.usage_calculation.fulfilled`; the
examples accept both spellings.

## Environment Variables

```bash
VELLUM_WEBHOOK_HEADER=Authorization   # header name you configured in Vellum
VELLUM_WEBHOOK_SECRET=Bearer your-token   # exact, whole header value

```

## Local Development

Vellum can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 vellum --path /webhooks/vellum

```

Use the printed URL as the webhook URL in Vellum.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vellum-webhooks/references/overview.md) - What Vellum webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vellum-webhooks/references/setup.md) - Configure the Webhook Integration and its authentication
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vellum-webhooks/references/verification.md) - Credential check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vellum-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests