Webhooks
Send events to external services when things happen in Velocity.
Overview
Outgoing webhooks send HTTP POST requests to a URL you specify whenever events occur in your workspace. Use webhooks to build custom integrations, trigger CI/CD pipelines, update external dashboards, or sync data with other tools.
Creating a Webhook
Go to Settings → Webhooks and click New Webhook. Provide:
- URL — the endpoint that will receive POST requests. It must be a public
http(s)address: private, loopback and cloud metadata addresses are refused, and redirects are not followed - Events — which events to send (e.g., issue.created, issue.updated)
- Secret — used to sign payloads (auto-generated if not provided)
Payload Format
{
"id": "delivery_uuid",
"event": "issue.updated",
"timestamp": "2026-01-15T10:30:00.000Z",
"workspace": { "id": "workspace_uuid" },
"data": {
"issue": {
"id": "issue_uuid",
"identifier": "ENG-42",
"title": "Fix login bug",
"team_id": "team_uuid",
"status_id": "status_uuid",
"priority": 1,
"assignee_id": "user_uuid",
"project_id": null,
"cycle_id": null,
"due_date": null,
"created_at": "2026-01-14T09:00:00.000Z",
"updated_at": "2026-01-15T10:30:00.000Z"
},
"changes": {
"status_id": { "from": "status_uuid_before", "to": "status_uuid" }
},
"actor_id": "user_uuid"
}
}priority is 0 (urgent) to 4 (none). changes appears on issue.updated and lists status, assignee and priority transitions. comment.created carries comment (id, body) alongside issue. Each delivery also sends the event name in X-Velocity-Event and the delivery ID in X-Velocity-Delivery.
issue.status_changed
Sent alongside issue.updated whenever an issue moves to another status, with both statuses spelled out. A status's type is its group: backlog, unstarted, started, completed or cancelled.
"data": {
"issue": { "id": "issue_uuid", "identifier": "ENG-42", "status_id": "status_done", … },
"change": { "from": "status_progress", "to": "status_done" },
"from_status": { "id": "status_progress", "name": "In Progress", "type": "started" },
"to_status": { "id": "status_done", "name": "Done", "type": "completed" },
"actor_id": "user_uuid"
}project.created and project.updated
project.updated is sent on every save of a project, and updated_fields lists the fields that save set (not necessarily changed).
"data": {
"project": {
"id": "project_uuid",
"name": "Website Redesign",
"identifier": "WR",
"description": null,
"status": "in_progress",
"lead_id": "user_uuid",
"start_date": "2026-09-01",
"target_date": "2026-12-01",
"created_at": "2026-08-20T09:00:00.000Z",
"updated_at": "2026-09-26T12:00:00.000Z"
},
"updated_fields": ["status"],
"actor_id": "user_uuid"
}project.created has the same shape without updated_fields.
cycle.started and cycle.completed
Sent when Velocity moves a cycle to active or completed by its dates. That happens hourly, so these arrive within an hour of midnight UTC on the cycle's start or end date (see Cycles). There is no actor_id.
"data": {
"cycle": {
"id": "cycle_uuid",
"name": "Sprint 14",
"team_id": "team_uuid",
"status": "active",
"start_date": "2026-09-22",
"end_date": "2026-10-06",
"created_at": "2026-09-01T09:00:00.000Z",
"updated_at": "2026-09-22T00:05:00.000Z"
}
}Signature Verification
Each delivery includes an X-Velocity-Signature header containing an HMAC-SHA256 signature of the raw request body, prefixed with sha256=.
Two details this example depends on. The signing key is the SHA-256 hex digest of your webhook secret, not the secret itself — derive it as shown below. And the signature covers the raw body bytes, so verify before parsing JSON: re-serializing the parsed object changes the bytes and the signature will not match.
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
// Velocity signs with the SHA-256 hex digest of your secret.
const key = crypto.createHash('sha256').update(secret).digest('hex');
const expected =
'sha256=' +
crypto.createHmac('sha256', key).update(rawBody).digest('hex');
const a = Buffer.from(signature || '');
const b = Buffer.from(expected);
// timingSafeEqual throws a RangeError when the buffers differ in
// length — which is the common case for a wrong signature. Compare
// lengths first so a mismatch returns false instead of crashing.
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Delivery History
The webhooks page shows a delivery history for each webhook with status codes, response times, and request/response bodies. Failed deliveries can be retried manually.
Auto-Disable
Webhooks that fail 10 consecutive deliveries are automatically disabled to prevent repeated failures. Re-enable them from the settings page after fixing the endpoint.
Unsubscribing with 410 Gone
A 410 Gone response is final: the delivery is not retried and doesn't count as a failure. A webhook created in Settings → Webhooks is turned off, and you can turn it back on there. A hook that Zapier, Make or another tool subscribed through the automation API is deleted. Zapier answers 410 once a Zap is turned off or deleted, so its hook goes away with it.
Hooks from Zapier and Make
Hooks subscribed by Zapier or through the automation API appear in this list too. Each belongs to the person who subscribed it and is removed when that person leaves the workspace. These hooks have no signing secret, so their deliveries carry an empty X-Velocity-Signature.
Available Events
| Event | Description |
|---|---|
issue.created | A new issue was created |
issue.updated | An issue was modified, including status and assignee changes |
issue.status_changed | An issue moved to another status (also sent as issue.updated) |
issue.deleted | An issue was deleted |
comment.created | A comment was added |
project.created | A project was created |
project.updated | A project was saved |
cycle.started | A cycle became active on its start date |
cycle.completed | A cycle was completed on its end date |