Agent skill

Velt Webhooks Skill

Receive and verify Velt webhooks (Advanced V2, Enterprise, signed with Standard Webhooks). Use when setting up Velt webhook handlers, debugging webhook-signature verification, or handling events like comment_annotation.add, comment.add, huddle.join, crdt.update_data, recorder.done, and execution.completed.

Install this skill

npx skills add hookdeck/webhook-skills --skill velt-webhooks


Velt is an SDK for in-app collaboration: comments, huddles, CRDT sync, recordings and review workflows. Its Advanced (V2) webhooks, available on Enterprise only, are delivered through Svix and signed with Standard Webhooks headers: webhook-id, webhook-timestamp and webhook-signature. The event name is the top-level event field of a JSON body shaped {actionType, data, event, platform, source, webhookId}.

Requirement. Hookdeck's configuration alert for this source reads: "Only Velt Advanced (V2) webhooks, available on Enterprise, are signed. Basic (V1) webhooks send an auth token instead and fail verification." Basic (V1) webhooks only send an optional auth token in the Authorization header as Basic <token>.

When to Use This Skill

  • How do I receive Velt webhooks?
  • How do I verify Velt webhook signatures (webhook-signature, whsec_ secret)?
  • How do I handle comment_annotation.add, comment.add, crdt.update_data or recorder.done events?
  • Why is my Velt webhook signature verification failing, or rejecting Hookdeck retries?
  • Why do Velt Basic (V1) webhooks fail signature verification?

Verification (core)

Signed content is {webhook-id}.{webhook-timestamp}.{raw body}. Velt signs it with HMAC-SHA256 using the base64-decoded secret (the part after whsec_), base64-encodes the result, and sends one or more space-separated v1,<signature> entries. Use a Standard Webhooks library (standardwebhooks on npm and PyPI), which compares in constant time. Read the webhook-* headers, and when webhook-id is absent fall back to the svix-* headers (svix-id, svix-timestamp, svix-signature), as Hookdeck does.

const { Webhook } = require('standardwebhooks');

const prefix = req.get('webhook-id') ? 'webhook' : 'svix';
const headers = {
  'webhook-id': req.get(`${prefix}-id`),
  'webhook-timestamp': req.get(`${prefix}-timestamp`),
  'webhook-signature': req.get(`${prefix}-signature`),
};
// rawBody: the raw Buffer/string, never re-serialized JSON
const event = new Webhook(process.env.VELT_WEBHOOK_SECRET).verify(rawBody, headers);
// throws on a bad signature or a timestamp outside the window; returns the parsed body
from standardwebhooks.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["VELT_WEBHOOK_SECRET"])  # "whsec_..."
event = wh.verify(raw_body, {"webhook-id": id_, "webhook-timestamp": ts, "webhook-signature": sig})

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Each endpoint has its own secret in the Velt console. It starts with whsec_. Keep the full value in VELT_WEBHOOK_SECRET. Handlers fail closed: with the variable unset they return 500 instead of accepting anything. A missing header or a bad signature returns 400. The raw body is required, so in Express use express.raw({ type: '*/*' }) and parse the body only after the check.

Hookdeck retries and replays hit the 5-minute window

The Standard Webhooks libraries always enforce a fixed 5-minute timestamp window, and it can't be configured. Velt re-signs each attempt with a fresh webhook-timestamp, but Hookdeck retries and replays resend the original timestamp, so the library rejects any retry or replay that happens more than 5 minutes after the original delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in references/verification.md, where the window can be dropped or widened. Deduplicate on webhook-id instead of relying on the timestamp.

Common Event Types

33 events are documented, and the list may not be exhaustive. The name is event in the JSON body. The examples handle all of these:

EventTriggered When
comment_annotation.addA comment thread is created with its first comment
comment_annotation.assignA thread is assigned to a user
comment_annotation.status_changeA thread's status changes
comment_annotation.priority_changeA thread's priority changes
comment_annotation.custom_list_changeA custom list item is added or changed on a thread
comment_annotation.subscribe / .unsubscribeA user subscribes to or leaves a thread
comment_annotation.accept / .reject / .approveA moderator acts on a thread (Moderator Mode only)
comment_annotation.suggestion_accept / .suggestion_rejectA suggestion is accepted or rejected (opt-in)
comment.add / .update / .deleteA comment is added, edited or removed
comment.reaction_add / .reaction_deleteA reaction is added or removed
huddle.create / huddle.joinA huddle is created or joined
crdt.update_dataCRDT data is updated (5-second debounce)
recorder.doneA recording finishes processing
execution.dispatched / .completed / .failed / .cancelledReview workflow execution lifecycle
step.awaiting-approval / .completed / .failed / .breached / .cancelledReview workflow step lifecycle
group.quorum-met, loop.iteration-started, loop.exhaustedReview workflow groups and loops

data holds actionUser, the feature object (commentAnnotation, crdtData, ...) and metadata (apiKey, document, organization, pageInfo). Encoded (Base64) or encrypted deliveries are opt-in and have no top-level event. See references/overview.md for the full list and shape.

Environment Variables

# Signing secret of the Advanced (V2) webhook endpoint (Standard Webhooks format, starts with whsec_)
VELT_WEBHOOK_SECRET=whsec_xxxxx

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 velt --path /webhooks/velt

Add the tunnel URL as an endpoint in the Velt console, then send an example event from its Testing tab.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →