Agent skill

Vellum Webhooks Skill

Receive and authenticate Vellum monitoring webhooks. Use when setting up a Vellum webhook endpoint, checking the API Key Header or Bearer Token credential, debugging 401s, or handling workflow.execution.initiated, workflow.execution.fulfilled, workflow.execution.rejected and metric.execution.fulfilled events.

Install this skill

npx skills add hookdeck/webhook-skills --skill 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)

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/, examples/nextjs/, 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.

nameSent When
workflow.execution.initiatedA workflow starts execution
workflow.execution.fulfilledA workflow completes successfully
workflow.execution.rejectedA workflow execution fails
workflow_execution.usage_calculation.fulfilledWorkflow usage is calculated
workflow_execution.usage_calculation.rejectedWorkflow usage calculation fails
metric.execution.fulfilledA metric execution completes successfully

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

Environment Variables

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):

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

Use the printed URL as the webhook URL in Vellum.

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →