Skip to content
Docs/Webhooks

Webhooks

Receive real-time HTTP notifications when events occur in your VEM projects.

What are Webhooks?

Webhooks allow external services to receive real-time notifications about events in your VEM projects. When an event occurs (like a task being completed or a snapshot being pushed), VEM sends an HTTP POST request to your specified endpoint with the event details.

Team Notifications

Send updates to Slack or Discord when tasks are completed

CI/CD Integration

Trigger deployments or tests when snapshots are verified

Custom Automation

Build custom workflows that react to project events

Getting Started

1

Create a Webhook

Navigate to /keys and click the "Webhooks" tab. Choose your scope (organization-wide or project-specific) and click "New Webhook".

2

Configure Events

Select which events you want to receive notifications for. You can subscribe to task lifecycle events, snapshot events, or other project events.

3

Save the Secret

After creating the webhook, you'll receive a signing secret. Save it securely - it won't be shown again. You'll use this secret to verify webhook authenticity.

4

Implement Your Endpoint

Set up an endpoint to receive webhook POST requests. Verify the signature and process the event data.

Available Events

Subscribe to any combination of these event types:

Task Events

task.created
task.started
task.blocked
task.completed
task.deleted

Snapshot Events

snapshot.pushed
snapshot.verified
snapshot.failed

Other Events

decision.added
changelog.updated
drift.detected
project.linked

Slack & Discord Support

VEM provides native formatting for Slack and Discord "out of the box". When you use a Slack or Discord webhook URL, VEM automatically detects it and transforms the payload into a visually rich message (Block Kit for Slack, Embeds for Discord) including color-coding and direct links back to your project.

#

Slack

Uses Slack Block Kit for structured headers, project context, and actionable buttons.

https://hooks.slack.com/services/...
D

Discord

Uses Discord Rich Embeds with color-coded sidebars and inline fields for task details.

https://discord.com/api/webhooks/...

Payload Structure

All webhook payloads follow this consistent structure:

{
  "event": "task.completed",
  "timestamp": "2026-02-06T12:00:00Z",
  "org_id": "org_abc123",
  "project_id": "proj_xyz789",
  "project_name": "my-project",
  "data": {
    // Event-specific payload
    "task_id": "TASK-001",
    "title": "Implement authentication",
    "status": "done",
    "evidence": ["pnpm test passed"],
    "url": "https://app.vem.dev/project/..."
  }
}

Signature Verification

VEM signs all webhook requests with HMAC SHA-256. You should verify the signature to ensure requests are authentic.

Important: Always verify webhook signatures in production to prevent unauthorized access to your endpoints.

Node.js / Express

const crypto = require('crypto');
const express = require('express');

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const secret = process.env.WEBHOOK_SECRET;

  // Generate expected signature
  const expectedSig = 'sha256=' +
    crypto.createHmac('sha256', secret)
      .update(req.body)
      .digest('hex');

  // Verify signature
  if (!crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSig)
  )) {
    return res.status(401).send('Invalid signature');
  }

  // Process webhook
  const payload = JSON.parse(req.body);
  console.log('Event:', payload.event);

  res.status(200).send('OK');
});

Python / Flask

import hmac
import hashlib
import os
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Webhook-Signature')
    secret = os.environ['WEBHOOK_SECRET']

    # Generate expected signature
    expected_sig = 'sha256=' + hmac.new(
        secret.encode(),
        request.data,
        hashlib.sha256
    ).hexdigest()

    # Verify signature
    if not hmac.compare_digest(signature, expected_sig):
        return 'Invalid signature', 401

    # Process webhook
    payload = request.json
    print(f"Event: {payload['event']}")

    return 'OK', 200

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
)

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Webhook-Signature")
    secret := os.Getenv("WEBHOOK_SECRET")

    // Read body
    body, _ := io.ReadAll(r.Body)

    // Generate expected signature
    h := hmac.New(sha256.New, []byte(secret))
    h.Write(body)
    expectedSig := "sha256=" + hex.EncodeToString(h.Sum(nil))

    // Verify signature
    if !hmac.Equal([]byte(signature), []byte(expectedSig)) {
        http.Error(w, "Invalid signature", 401)
        return
    }

    // Process webhook
    var payload map[string]interface{}
    json.Unmarshal(body, &payload)
    fmt.Printf("Event: %s\n", payload["event"])

    w.WriteHeader(200)
}

Slack Integration

Send notifications to Slack using Incoming Webhooks:

1

Create Incoming Webhook in Slack

Go to your Slack workspace settings → Apps → Add apps → Search for "Incoming Webhooks" → Add to Slack → Choose a channel

View Slack Documentation
2

Copy Webhook URL

Slack will provide a URL like: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX

3

Add to VEM

In VEM, create a new webhook with the Slack URL. However, Slack expects a specific message format. You'll need a middleware service to transform VEM webhooks into Slack's format.

Note: Direct Slack integration requires message transformation. Consider using a service like Zapier or building a simple transformation endpoint.

Discord Integration

Send notifications to Discord channels:

1

Create Webhook in Discord

In your Discord server: Channel Settings → Integrations → Webhooks → New Webhook → Copy Webhook URL

2

Transform Payload

Like Slack, Discord expects a specific format. You'll need a transformation service.

Note: Consider using a serverless function or service to transform VEM webhook payloads into Discord's embed format.

Best Practices

Always Verify Signatures

Never trust webhook data without verifying the HMAC signature. This prevents unauthorized access.

Respond Quickly

Return a 200 status code within 10 seconds. Process webhook data asynchronously if needed.

Handle Retries

VEM retries failed webhooks up to 3 times. Implement idempotency using the X-Webhook-Delivery ID.

Use HTTPS

Webhook endpoints must use HTTPS in production for security.

Monitor Delivery History

Check the delivery history in your webhook settings to debug issues.

Troubleshooting

Webhook deliveries are failing

• Verify your endpoint URL is correct and accessible

• Check that your endpoint responds within 10 seconds

• Ensure your endpoint returns a 2xx status code

• Review delivery history for error messages

Signature verification is failing

• Verify you're using the correct secret from webhook creation

• Ensure you're reading the raw request body, not parsed JSON

• Check that you're comparing the exact signature format (sha256=...)

• Use crypto.timingSafeEqual or equivalent for secure comparison

Not receiving webhook events

• Verify the webhook is enabled (check toggle in settings)

• Confirm you've subscribed to the correct events

• Check that events are actually occurring in your project

• For project-level webhooks, ensure you selected the right project

Receiving duplicate events

• This is expected behavior during retries after failures

• Implement idempotency using the X-Webhook-Delivery ID header

• Store processed delivery IDs to skip duplicates

Next Steps