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
Create a Webhook
Navigate to /keys and click the "Webhooks" tab. Choose your scope (organization-wide or project-specific) and click "New Webhook".
Configure Events
Select which events you want to receive notifications for. You can subscribe to task lifecycle events, snapshot events, or other project events.
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.
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.createdtask.startedtask.blockedtask.completedtask.deletedSnapshot Events
snapshot.pushedsnapshot.verifiedsnapshot.failedOther Events
decision.addedchangelog.updateddrift.detectedproject.linkedSlack & 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/...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', 200Go
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:
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 DocumentationCopy Webhook URL
Slack will provide a URL like: https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXX
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.
Discord Integration
Send notifications to Discord channels:
Create Webhook in Discord
In your Discord server: Channel Settings → Integrations → Webhooks → New Webhook → Copy Webhook URL
Transform Payload
Like Slack, Discord expects a specific format. You'll need a transformation service.
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