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.rejectedormetric.execution.fulfilledevents? - 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, defaultAuthorization). The expected value (VELLUM_WEBHOOK_SECRET) is the exact, whole header value, including anyBearerprefix you typed. Look the header up case-insensitively and compare the whole value; don't strip or parseBearer. - Missing header or mismatch:
401. Check it first, for every HTTP method. - Python:
hmac.compare_digeston bytes (encode both sides;strraises 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_. 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_ | Workflow usage is calculated |
workflow_ | Workflow usage calculation fails |
metric.execution.fulfilled | A metric execution completes successfully |
Vellum's schema example spells the usage event workflow.usage_; 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
- references/overview.md - What Vellum webhooks are, events and payload shape
- references/setup.md - Configure the Webhook Integration and its authentication
- references/verification.md - Credential check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests