Guide to Vellum Webhooks: Features and Best Practices
Vellum webhooks stream what happens in your Vellum workspace to your own systems: a workflow starts, finishes or fails, Vellum works out the token usage and cost of a run, or a metric finishes evaluating. If you run LLM workflows in Vellum, webhooks let you alert on failures, track spend and keep an audit log in the tools you already use.
This guide covers how Vellum's monitoring webhooks are set up, the events and envelope they use, how to authenticate them with a static credential, where Vellum's optional HMAC signature fits, and what to plan for given how little Vellum documents about delivery.
What are Vellum webhooks?
Vellum's webhooks are part of its monitoring features. You configure one Webhook Integration for the organisation, choose which events it sends, and Vellum sends each event to your URL as JSON.
Authentication is a credential that Vellum adds to every request. Vellum offers two kinds:
- API Key Header: a header name and value that you choose.
- Bearer Token: a token sent in the
Authorizationheader asBearer <token>.
Vellum can also sign outgoing requests with HMAC. Once you provide an HMAC token in your workspace settings, Vellum adds X-Vellum-Timestamp and X-Vellum-Signature headers to webhooks and to API node requests. The signature covers the request URL as well as the body, which matters if a gateway sits in front of your service (more on that below).
Vellum webhook features
| Feature | Details |
|---|---|
| Configuration | Organisation settings, Monitoring Integration section, Webhook Integration |
| Events | Six selectable workflow, usage and metric events |
| Payload | JSON envelope with id, name, timestamp, parent and body |
| Authentication | API Key Header (custom header) or Bearer Token |
| Signature | Optional HMAC-SHA256 in X-Vellum-Signature, over timestamp, method, URL and body |
| Method | Not documented |
| Retries | Not documented. Vellum tells you to expect retries and duplicates |
| Timeout | Not documented |
| API version | 2024-10-25 in each event's api_version |
Common events
| Category | Event | Sent when |
|---|---|---|
| Workflow execution | workflow.execution.initiated | A workflow starts |
workflow.execution.fulfilled | A workflow completes successfully | |
workflow.execution.rejected | A workflow execution fails | |
| Usage calculation | workflow_ | Usage for a run is calculated |
workflow_ | Usage calculation fails | |
| Metrics | metric.execution.fulfilled | A metric execution completes successfully |
Every event uses the same envelope. Here is a failed workflow run:
{
"id": "7c1d2a4e-3b9f-4f6e-9a51-2d8c0e4b6f10",
"timestamp": "2026-10-26T09:14:03.512Z",
"api_version": "2024-10-25",
"trace_id": "0b6c3f1e-8d2a-4c7b-a5e9-4f1d2c3b6a70",
"span_id": "e4a9b2c1-6d3f-4e8a-b7c5-1f2e3d4c5b60",
"parent": {
"deployment_id": "a3f8c2d1-5e6b-4a7c-9d8e-0f1a2b3c4d50",
"deployment_name": "support-triage",
"release_tag_name": "PRODUCTION",
"external_id": "ticket-48213"
},
"name": "workflow.execution.rejected",
"body": {
"workflow_class": { "name": "SupportTriageWorkflow", "module": "support_triage", "bases": [] },
"error": { "code": "NODE_EXECUTION", "message": "Upstream model timed out" }
}
}
Dispatch on name. parent tells you which deployment and release tag the run came from, and external_id can carry your own identifier for the run, which is the easiest way to tie an event back to your own records. initiated events carry the workflow's inputs, fulfilled events carry its outputs, and usage events carry token and character counts, compute time and a cost in USD.
Two things to watch. First, the event list names the usage events workflow_, but Vellum's example payload for the same event shows workflow.usage_, so accept both spellings until you've seen a real one. Second, inputs and outputs are whatever your workflow received and produced, which can include prompts, customer messages and model output. Treat the webhook stream as sensitive data.
Setting up Vellum webhooks
Generate a long random secret first, for example with openssl rand -hex 32, and store it server-side.
- In Vellum, open your organisation settings page.
- Scroll to the Monitoring Integration section and click Webhook Integration, or Edit if one already exists.
- Enter your HTTPS endpoint as the URL.
- Turn on authentication and choose one option:
- API Key Header: enter a header name, such as
X-Vellum-Webhook-Secret, and your secret as the value. - Bearer Token: enter your secret as the token. Vellum sends it as
Authorization: Bearer <token>.
- API Key Header: enter a header name, such as
- Choose the events to send. Starting with
workflow.execution.rejectedgives you failure alerts straight away. - Save the integration.
With Hookdeck. Vellum is a supported source in Hookdeck. Create a source with the Vellum type and use the Hookdeck source URL as the webhook URL in Vellum. Hookdeck's setup note says to turn on authentication and choose API Key Header, or Bearer Token. For API Key Header, enter the same header name as the API Key Header and the secret as the API Key. For Bearer Token, enter Authorization as the API Key Header and Bearer <token> as the API Key, with the Bearer prefix included. The HMAC signature option isn't supported, so the source checks only the header. The source pre-selects POST without managing the method. Vellum's docs don't say which method its webhooks use, so if the request log shows Vellum using something else, allow that method on the source as well, because the source only accepts the methods you allow. A request without the right header still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.
Securing Vellum webhooks
Compare the credential header with the stored value in constant time. Look the header up case-insensitively, compare the whole value, including Bearer if you use a bearer token, and check lengths before calling crypto.timingSafeEqual.
const crypto = require('crypto');
const express = require('express');
function safeEqual(a, b) {
const x = Buffer.from(String(a), 'utf8');
const y = Buffer.from(String(b), 'utf8');
return x.length === y.length && crypto.timingSafeEqual(x, y);
}
function verifyVellumRequest(headers) {
const expected = process.env.VELLUM_WEBHOOK_SECRET; // e.g. "Bearer <token>" or the raw API key
if (!expected) return false; // fail closed when unconfigured
const name = (process.env.VELLUM_WEBHOOK_HEADER || 'Authorization').toLowerCase();
const received = headers[name];
return typeof received === 'string' && safeEqual(received, expected);
}
const app = express();
app.post('/webhooks/vellum', express.raw({ type: '*/*' }), (req, res) => {
if (!verifyVellumRequest(req.headers)) {
return res.sendStatus(401);
}
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Queue the event, keyed on event.id, then dispatch on event.name.
res.sendStatus(200);
});
The same check in Python:
import hmac
import os
def verify_vellum_request(headers) -> bool:
expected = os.environ.get("VELLUM_WEBHOOK_SECRET")
if not expected:
return False
name = os.environ.get("VELLUM_WEBHOOK_HEADER", "Authorization")
received = headers.get(name) # case-insensitive in Starlette and Flask
if not received:
return False
return hmac.compare_digest(received.encode("utf-8"), expected.encode("utf-8"))
A static credential proves the sender knows the secret, but it doesn't prove the body is unchanged, and it gives no replay protection. Use HTTPS, keep the secret long and random, and rotate it if it leaks.
Vellum's HMAC signature closes those gaps when Vellum calls your service directly. The signed message is the timestamp, the method, the URL and the body, joined by newlines, and Vellum's docs tell you to reject timestamps older than 60 seconds. Behind a gateway it's harder to use. The URL Vellum signed is the gateway's URL, not your endpoint's, so your check would need to rebuild the message with the Hookdeck source URL. And any delivery that reaches you more than 60 seconds after Vellum sent it, such as a retry or a replay, fails Vellum's timestamp check. If you route Vellum through Hookdeck, rely on the header credential that the source checks.
Make Vellum webhooks production-ready. Hookdeck Event Gateway checks your Vellum credential on every request, stores each workflow, usage and metric event, and retries delivery to your service on a schedule you control.
Vellum webhook limitations and pain points
Delivery behaviour isn't documented
The Problem: You can't tell from the docs how long Vellum waits for a response, how often it retries, or when it gives up.
Why It Happens: Vellum's webhook page covers configuration, events and authentication. Its only guidance on delivery is to "handle occasional failures or retries" and to make handlers idempotent "in case of duplicate events".
Workarounds:
- Return
2xxquickly and do the work asynchronously, so an unknown timeout doesn't matter. - Assume a failed delivery may not come back, and reconcile important runs from Vellum's own execution history.
How Hookdeck Can Help: Hookdeck answers Vellum as soon as a request arrives and stores it, then delivers it to your service with automatic retries on a schedule you set. Issues alert you when deliveries fail, and you can replay stored events after a fix.
Duplicate events
The Problem: The same event can arrive more than once and trigger the same alert or cost record twice.
Why It Happens: Vellum says to expect duplicates, and any sender that retries will sometimes resend an event your service already processed.
Workarounds:
- Record each event's
idand skip ones you've already handled.
How Hookdeck Can Help: Deduplication can drop repeat requests before they reach your service, for example on the body's id field.
HMAC breaks behind a gateway
The Problem: Turning on Vellum's HMAC token doesn't add protection you can use once Hookdeck sits in front of your service.
Why It Happens: The signature covers the URL Vellum called, and the docs ask you to reject signatures older than 60 seconds. Hookdeck's Vellum source doesn't check the signature, so the check falls to your service, where the URL is different and retries arrive late.
Workarounds:
- Use the API Key Header or Bearer Token credential, and check it at the gateway and at your service.
- If you need HMAC, send Vellum directly to your service and verify it there.
How Hookdeck Can Help: The Vellum source checks the header credential on every request and keeps a log of each one, including those that fail verification, so you can see when a credential is wrong.
One integration, many event types
The Problem: Failure alerts, cost tracking and metric results all arrive at the same URL, and each needs different handling.
Why It Happens: There is one Webhook Integration per organisation, and the event name sits in the body, not in the URL.
Workarounds:
- Dispatch on
namein a single handler. - Split by
parent.deployment_nameorparent.release_tag_nameto keep staging traffic out of production alerts.
How Hookdeck Can Help: Hookdeck filters can route on name or parent.release_tag_name, so workflow.execution.rejected goes to your alerting service and usage events go to your billing pipeline.
Best practices
Start with failures
Send workflow.execution.rejected first and alert on it. initiated and fulfilled events double your volume for every run, so add them when you have a use for them.
Use the external ID
If you run deployments with an external ID, look for it in parent.external_id. It ties each event to your own ticket, user or job without a lookup.
Accept both usage event spellings
Handle workflow_ and workflow.usage_ until a real event confirms which one Vellum sends.
Build idempotency in
Key on the event id. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Check the credential, store the event, return 200, then do the work. See why to process webhooks asynchronously.
Conclusion
Vellum sends monitoring webhooks for workflow runs, usage calculations and metrics, each in a JSON envelope with an id, a name and the deployment it came from. Authenticate them with an API key header or bearer token compared in constant time over HTTPS, dedupe on id, and dispatch on name. Vellum's optional HMAC signature is useful when it calls your service directly, but it covers the URL and a 60-second window, which doesn't suit a gateway.
Hookdeck Event Gateway checks the credential, answers Vellum straight away, and retries delivery to your service, which fills the gaps in Vellum's undocumented delivery behaviour.
Get started with Hookdeck for free and handle Vellum webhooks reliably in minutes.