Skip to content
Docs/Webhooks

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.

Available on Pro and Enterprise plans

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

  1. 1Create a webhook in Project → Settings → Webhooks or via the API.
  2. 2Copy the secret shown once at creation time — it is never shown again.
  3. 3Receive POST requests at your endpoint for each subscribed event.
  4. 4Verify the X-Vem-Signature header and return HTTP 2xx within 10 s.

Project vs Org webhooks

Webhooks can be scoped to a single project or to your entire organization.

ScopeFires on
ProjectEvents in one project only
OrgEvents across all projects

Event types

Task Events
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.
Agent Run Events
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 Events
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.
Memory Events
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.reserved
Cycle Events
cycle.createdA new work cycle is created in a project.
cycle.startedA cycle transitions to active status.
cycle.closedA cycle transitions to closed status.
Project & Team Events
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": { ... }
}
FieldTypeDescription
eventstringThe event type (e.g. task.created)
timestampISO 8601UTC time the event was dispatched
org_idstringOrganization that owns the event
project_idstring | nullProject context (null for org-level events)
project_namestring?Human-readable project name when available
dataobjectEvent-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.

HMAC-SHA256

Keyed with your per-webhook secret (wh_sec_...).

Constant-time compare

Use timingSafeEqual or hmac.compare_digest — never == or ===.

Raw body

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", 200

Delivery & retries

3 attempts

vem retries failed deliveries up to 3 times with exponential back-off.

HTTP 2xx = success

Any 2xx response within 10 seconds marks the delivery as successful.

Fire-and-forget

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.

GET/api/v1/projects/:projectId/webhooks

List webhooks for a project.

GET/api/v1/orgs/:orgId/webhooks

List org-level webhooks (fires across all projects).

POST/api/v1/projects/:projectId/webhooks

Create 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_..."
}
POST/api/v1/orgs/:orgId/webhooks

Create an org-level webhook.

Body

{ "url": "...", "events": ["task.created", "member.joined"] }
PATCH/api/v1/webhooks/:webhookId

Update URL, events, or enabled state.

Body

{ "url": "https://new-url.com/hook", "enabled": false }
DELETE/api/v1/webhooks/:webhookId

Delete a webhook and its delivery history.

GET/api/v1/webhooks/:webhookId/deliveries?limit=50

Fetch 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 timingSafeEqual not ===.
  • 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 id field in the delivery history response.
403 on webhook creation
  • Webhooks require a Pro or Enterprise plan.
  • Upgrade in Org → Billing.

Next steps