Webhooks
Subscribe to real-time events from your vem projects and organization. When an event fires, vem sends a signed HTTP POST to your endpoint.
Team notifications
Post to Slack or Discord when tasks complete or agents finish a run.
CI/CD triggers
Kick off deployments or tests when snapshots are verified against a commit.
Custom workflows
Build any automation that reacts to project, task, or agent activity.
Quickstart
- 1Create a webhook in Project → Settings → Webhooks or via the API.
- 2Copy the secret shown once at creation time — it is never shown again.
- 3Receive
POSTrequests at your endpoint for each subscribed event. - 4Verify the
X-Vem-Signatureheader and return HTTP 2xx within 10 s.
Project vs Org webhooks
Webhooks can be scoped to a single project or to your entire organization.
| Scope | Fires on |
|---|---|
| Project | Events in one project only |
| Org | Events across all projects |
Event types
task.createdA new task is created in a project.task.updatedNon-status fields change (title, priority, assignee, tags...).task.startedTask status transitions to in-progress.task.blockedTask status transitions to blocked.task.completedTask status transitions to done.task.deletedA task is soft-deleted.task_run.startedAn agent claims and starts a task run.task_run.completedA task run finishes with a completed outcome.task_run.failedA task run finishes with a failed outcome.task_run.cancelledA task run is cancelled before completion.snapshot.pushedA vem push snapshot is ingested by the API.snapshot.verifiedA GitHub webhook confirms the matching git commit.snapshot.failedA commit carries a vem hash but no matching pending snapshot is found.decision.addedA snapshot push contains new or changed decisions content.changelog.updatedA snapshot push contains new or changed changelog content.drift.detectedContext drift detected between snapshots.reservedcycle.createdA new work cycle is created in a project.cycle.startedA cycle transitions to active status.cycle.closedA cycle transitions to closed status.project.createdA new project is created in the organization.project.deletedA project is deleted.project.linkedA GitHub repository is linked to a project for the first time.member.invitedA new member invitation is sent.member.joinedAn invited member accepts and joins the organization.Payload format
Every delivery is a POST with Content-Type: application/json.
Envelope
{
"event": "task.created",
"timestamp": "2026-03-30T20:33:44.863Z",
"org_id": "org_abc123",
"project_id": "27369f17-048c-4459-b1e8-828760b278e3",
"project_name": "My Project",
"data": { ... }
}| Field | Type | Description |
|---|---|---|
event | string | The event type (e.g. task.created) |
timestamp | ISO 8601 | UTC time the event was dispatched |
org_id | string | Organization that owns the event |
project_id | string | null | Project context (null for org-level events) |
project_name | string? | Human-readable project name when available |
data | object | Event-specific fields (see below) |
Event-specific data fields
task.*
{
"task_id": "TASK-29",
"title": "Implement auth flow",
"status": "in-progress",
"priority": "high",
"url": "https://vem.dev/project/..."
}task_run.*
{
"run_id": "run_uuid",
"task_id": "TASK-29",
"agent_name": "claude-agent",
"outcome": "completed",
"url": "https://vem.dev/project/..."
}snapshot.pushed
{
"snapshot_id": "snap_uuid",
"git_hash": "abc1234",
"snapshot_hash": "sha256:def456...",
"task_count": 12,
"url": "https://vem.dev/project/.../map"
}snapshot.verified / snapshot.failed
{
"snapshot_id": "snap_uuid",
"git_hash": "abc1234",
"snapshot_hash": "sha256:def456...",
"url": "https://vem.dev/project/.../map"
}decision.added / changelog.updated
{
"snapshot_id": "snap_uuid",
"summary": "First line of content (up to 120 chars)",
"url": "https://vem.dev/project/.../map"
}cycle.*
{
"cycle_id": "cycle_uuid",
"name": "Sprint 4",
"status": "active",
"url": "https://vem.dev/project/..."
}project.* / member.*
{
"project_id": "proj_uuid",
"name": "My Project",
"github_repo": "owner/repo",
"email": "[email protected]",
"role": "member"
}Signature verification
Every delivery includes an X-Vem-Signature header — an HMAC-SHA256 hex digest of the raw request body keyed with your webhook secret. Always verify this before processing.
Keyed with your per-webhook secret (wh_sec_...).
Use timingSafeEqual or hmac.compare_digest — never == or ===.
Hash the raw bytes before JSON parsing. Re-serialised JSON changes the signature.
TypeScript / Node.js
import crypto from "node:crypto";
function verifySignature(secret: string, rawBody: string, signature: string): boolean {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
// Express example
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-vem-signature"] as string;
if (!verifySignature(process.env.WEBHOOK_SECRET!, req.body.toString(), sig)) {
return res.status(401).send("Invalid signature");
}
const payload = JSON.parse(req.body.toString());
console.log("Event:", payload.event);
res.status(200).send("OK");
});Python
import hmac, hashlib
def verify_signature(secret: str, raw_body: bytes, signature: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route("/webhook", methods=["POST"])
def webhook():
sig = request.headers.get("X-Vem-Signature", "")
if not verify_signature(os.environ["WEBHOOK_SECRET"], request.data, sig):
return "Invalid signature", 401
print(f"Event: {request.json['event']}")
return "OK", 200Delivery & retries
vem retries failed deliveries up to 3 times with exponential back-off.
Any 2xx response within 10 seconds marks the delivery as successful.
Deliveries are non-blocking — they never delay the operation that triggered them.
Delivery history (last 100 per webhook) is available in Settings → Webhooks → Deliveries. Use the delivery id field for idempotency when handling retries.
API reference
All endpoints require Authorization: Bearer <api_key> and X-Vem-Device-Id.
/api/v1/projects/:projectId/webhooksList webhooks for a project.
/api/v1/orgs/:orgId/webhooksList org-level webhooks (fires across all projects).
/api/v1/projects/:projectId/webhooksCreate a project webhook. "secret" is returned once only — store it securely.
Body
{
"url": "https://example.com/webhook",
"events": ["task.created", "task.completed", "snapshot.pushed"]
}Response
{
"webhook": { "id": "...", "url": "...", "events": [...], "enabled": true },
"secret": "wh_sec_..."
}/api/v1/orgs/:orgId/webhooksCreate an org-level webhook.
Body
{ "url": "...", "events": ["task.created", "member.joined"] }/api/v1/webhooks/:webhookIdUpdate URL, events, or enabled state.
Body
{ "url": "https://new-url.com/hook", "enabled": false }/api/v1/webhooks/:webhookIdDelete a webhook and its delivery history.
/api/v1/webhooks/:webhookId/deliveries?limit=50Fetch delivery history (max 100 per request).
Response
{
"deliveries": [
{
"id": "cdbb9a66-...",
"event_type": "task.created",
"status_code": 200,
"success": true,
"attempt": 1,
"error_message": null,
"delivered_at": "2026-03-30T20:33:44.863Z"
}
],
"total": 6
}Troubleshooting
Signature mismatch (401)
- Hash the raw body bytes before JSON parsing.
- Use
timingSafeEqualnot===. - Confirm the secret matches the one returned at webhook creation.
Events not arriving
- Check that the webhook is enabled in Settings.
- Confirm you subscribed to the correct event type.
- For project webhooks, verify the right project is selected.
- Check delivery history for failed attempts and error messages.
Duplicate events on retry
- Expected — deliveries are retried up to 3 times on failure.
- Deduplicate using the
idfield in the delivery history response.
403 on webhook creation
- Webhooks require a Pro or Enterprise plan.
- Upgrade in Org → Billing.