Webhooks
Receive real-time event notifications via HTTP callbacks
WEBHOOKS
Webhooks enable real-time integration by sending HTTP POST requests to your endpoints when events occur in CAPYSQUASH.
Build custom integrations and automation with event-driven webhooks
OVERVIEW
Webhooks provide a way to receive instant notifications when something happens in your CAPYSQUASH organization.
Use Cases:
- Trigger CI/CD pipelines on analysis completion
- Log events to external systems
- Send custom notifications
- Update project management tools
- Sync data to your database
- Build custom dashboards
CREATING A WEBHOOK
SETUP WEBHOOK IN 4 STEPS
CREATE ENDPOINT
Set up an HTTP endpoint on your server to receive webhooks
REGISTER WEBHOOK
- ► Dashboard → Settings → Webhooks
- ► Click "Add Webhook"
- ► Enter your endpoint URL
- ► Select events to receive
VERIFY SIGNATURE
Save the webhook secret and verify signatures in your code
TEST
Send test webhook to verify everything works
WEBHOOK EVENTS
Analysis Events
analysis.completed
{
"event": "analysis.completed",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"run_id": "run_abc123",
"project_id": "proj_123",
"project_name": "Production API",
"user_id": "user_456",
"user_name": "Jane Doe",
"original_count": 156,
"optimized_count": 12,
"reduction_percentage": 92,
"warning_count": 2,
"safety_level": "standard",
"processing_time_ms": 2341,
"dashboard_url": "https://capysquash.dev/runs/run_abc123"
}
}analysis.failed
{
"event": "analysis.failed",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"project_id": "proj_123",
"project_name": "Production API",
"user_id": "user_456",
"error_message": "Invalid SQL syntax in migration 047",
"error_code": "PARSE_ERROR"
}
}Usage Events
usage.limit.warning
{
"event": "usage.limit.warning",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"limit_type": "analyses",
"used": 40,
"limit": 50,
"percentage": 80,
"plan": "creator"
}
}usage.limit.exceeded
{
"event": "usage.limit.exceeded",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"limit_type": "projects",
"used": 3,
"limit": 3,
"plan": "free"
}
}Team Events
team.member_added
{
"event": "team.member_added",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"user_id": "user_789",
"user_name": "John Smith",
"user_email": "john@company.com",
"role": "member",
"invited_by": "user_456",
"inviter_name": "Jane Doe"
}
}team.member_removed
{
"event": "team.member_removed",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"user_id": "user_789",
"user_email": "john@company.com",
"removed_by": "user_456",
"reason": "left_organization"
}
}Project Events
project.created
{
"event": "project.created",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"project_id": "proj_789",
"project_name": "Staging API",
"creator_id": "user_456",
"creator_name": "Jane Doe",
"safety_level": "standard",
"database_provider": "neon"
}
}project.deleted
{
"event": "project.deleted",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"project_id": "proj_789",
"project_name": "Old Project",
"deleted_by": "user_456",
"runs_count": 42
}
}Subscription Events
subscription.changed
{
"event": "subscription.changed",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"old_plan": "creator",
"new_plan": "professional",
"changed_by": "user_456",
"effective_date": "2025-10-20T15:30:00Z"
}
}subscription.payment_failed
{
"event": "subscription.payment_failed",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"plan": "professional",
"amount": 2900,
"currency": "usd",
"failure_code": "card_declined",
"failure_message": "Your card was declined"
}
}WEBHOOK PAYLOAD STRUCTURE
All webhooks follow this structure:
interface WebhookPayload {
// Event identifier (unique per webhook delivery)
id: string;
// Event type
event: string;
// When event occurred (ISO 8601)
timestamp: string;
// Your organization ID
organization_id: string;
// Event-specific data
data: Record<string, any>;
}SIGNATURE VERIFICATION
Security Critical
Always verify webhook signatures to ensure requests are from CAPYSQUASH and haven't been tampered with.
How Signatures Work
CAPYSQUASH signs each webhook with HMAC SHA-256:
- We create a signature using your webhook secret
- Signature is sent in
X-CAPYSQUASH-Signatureheader - You compute expected signature and compare
- If they match, webhook is authentic
Verification Code Examples
import crypto from 'crypto';
function verifyWebhook(
payload: string,
signature: string,
secret: string
): boolean {
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
);
}
// Express.js example
app.post('/webhooks/capysquash', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-capysquash-signature'] as string;
const secret = process.env.CAPYSQUASH_WEBHOOK_SECRET!;
if (!verifyWebhook(req.body, signature, secret)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Webhook received:', event.event);
res.json({ received: true });
});import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
from flask import Flask, request, jsonify
@app.route('/webhooks/capysquash', methods=['POST'])
def webhook():
signature = request.headers.get('X-CAPYSQUASH-Signature')
secret = os.environ['CAPYSQUASH_WEBHOOK_SECRET']
if not verify_webhook(request.data, signature, secret):
return jsonify({'error': 'Invalid signature'}), 401
event = request.json
print(f"Webhook received: {event['event']}")
return jsonify({'received': True})package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifyWebhook(payload []byte, signature, secret string) bool {
h := hmac.New(sha256.New, []byte(secret))
h.Write(payload)
expected := hex.EncodeToString(h.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("X-CAPYSQUASH-Signature")
secret := os.Getenv("CAPYSQUASH_WEBHOOK_SECRET")
payload, _ := io.ReadAll(r.Body)
if !verifyWebhook(payload, signature, secret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook...
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"received": true}`))
}require 'openssl'
def verify_webhook(payload, signature, secret)
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, payload)
Rack::Utils.secure_compare(signature, expected)
end
# Sinatra example
post '/webhooks/capysquash' do
payload = request.body.read
signature = request.env['HTTP_X_CAPYSQUASH_SIGNATURE']
secret = ENV['CAPYSQUASH_WEBHOOK_SECRET']
unless verify_webhook(payload, signature, secret)
status 401
return { error: 'Invalid signature' }.to_json
end
event = JSON.parse(payload)
puts "Webhook received: #{event['event']}"
{ received: true }.to_json
endWEBHOOK CONFIGURATION
Endpoint URL
Requirements:
- Must be HTTPS (not HTTP)
- Must be publicly accessible
- Must return within 10 seconds
- Must return 2xx status code
Examples:
☑ https://api.yourapp.com/webhooks/capysquash
☑ https://yourapp.com/api/v1/webhooks
☒ http://api.yourapp.com/webhooks (not HTTPS)
☒ http://localhost:3000/webhooks (not public)Event Selection
Choose which events to receive:
All events: Receive every event type Selective: Choose specific events only
Recommendation: Only subscribe to events you need to reduce noise
Headers
Add custom headers to webhook requests:
Use cases:
- Additional authentication
- API versioning
- Content negotiation
Example:
X-API-Key: your-api-key
X-API-Version: 2024-10-20
Content-Type: application/jsonTimeout
Maximum time to wait for response:
- Default: 5 seconds
- Maximum: 10 seconds
Recommendation: Keep endpoints fast (<1s)
Retry Policy
Configure automatic retries on failure:
Settings:
- Enable/disable retries
- Max retry attempts (1-5)
- Backoff strategy (linear/exponential)
Default: 3 retries with exponential backoff
TESTING WEBHOOKS
Send Test Event
Test your webhook before enabling:
- Go to Webhooks → Your webhook
- Click "Send Test Event"
- Select event type
- Click "Send"
- Check your endpoint received it
Test payload example:
{
"event": "test.webhook",
"timestamp": "2025-10-20T15:30:00Z",
"organization_id": "org_xyz",
"data": {
"message": "This is a test webhook",
"test": true
}
}Webhook Logs
View delivery history:
Information shown:
- Timestamp
- Event type
- HTTP status code
- Response time
- Response body (if error)
- Retry attempts
Filters:
- By event type
- By status (success/failed)
- By date range
WEBHOOK BEST PRACTICES
WEBHOOK BEST PRACTICES
☑ DO
- ► Always verify signatures
- ► Return 200 status quickly (<1s)
- ► Process async (queue jobs)
- ► Use HTTPS endpoints only
- ► Log all webhook receipts
- ► Handle idempotency (deduplicate by
id) - ► Monitor for failures
☒ DON'T
- ► Skip signature verification
- ► Block webhook response with long processing
- ► Use HTTP (not HTTPS)
- ► Expose webhook secret in code
- ► Ignore retry logic
- ► Process same webhook multiple times
Async Processing Pattern
☒ Bad (blocks response):
app.post('/webhook', async (req, res) => {
// Don't do this - blocks for too long
await processLongTask(req.body);
await updateDatabase(req.body);
await sendNotifications(req.body);
res.json({ received: true });
});☑ Good (responds quickly):
app.post('/webhook', async (req, res) => {
// Verify signature
if (!verifySignature(req)) {
return res.status(401).end();
}
// Queue for processing
await queue.add('process-webhook', req.body);
// Respond immediately
res.json({ received: true });
});
// Process in background worker
worker.process('process-webhook', async (job) => {
await processLongTask(job.data);
await updateDatabase(job.data);
await sendNotifications(job.data);
});Idempotency
Webhooks may be delivered multiple times. Handle duplicates:
const processedWebhooks = new Set();
app.post('/webhook', async (req, res) => {
const webhookId = req.body.id;
// Check if already processed
if (processedWebhooks.has(webhookId)) {
console.log('Duplicate webhook, ignoring');
return res.json({ received: true });
}
// Mark as processed
processedWebhooks.add(webhookId);
// Process webhook...
await queue.add('process-webhook', req.body);
res.json({ received: true });
});Better: Store in database with unique constraint on id
USE CASE EXAMPLES
1. Trigger CI/CD Pipeline
When analysis completes, trigger deployment:
app.post('/webhook', async (req, res) => {
if (req.body.event === 'analysis.completed') {
const { project_name, reduction_percentage } = req.body.data;
if (reduction_percentage >= 50) {
// Trigger deployment pipeline
await triggerDeployment(project_name);
}
}
res.json({ received: true });
});2. Log to External System
Send events to logging service:
app.post('/webhook', async (req, res) => {
// Log to Datadog/Splunk/etc
await logger.info('CAPYSQUASH event', {
event: req.body.event,
org_id: req.body.organization_id,
data: req.body.data,
});
res.json({ received: true });
});3. Update Project Management
Create tasks for warnings:
app.post('/webhook', async (req, res) => {
if (req.body.event === 'analysis.completed') {
const { warning_count, project_name, dashboard_url } = req.body.data;
if (warning_count > 0) {
await jira.createIssue({
title: `Review ${warning_count} warnings in ${project_name}`,
description: `Analysis found warnings. Review at: ${dashboard_url}`,
labels: ['database', 'migrations', 'warnings'],
});
}
}
res.json({ received: true });
});4. Send Custom Notifications
Notify on Telegram/Discord:
app.post('/webhook', async (req, res) => {
if (req.body.event === 'analysis.completed') {
const { project_name, reduction_percentage } = req.body.data;
await telegram.sendMessage({
chat_id: process.env.TELEGRAM_CHAT_ID,
text: `☑ ${project_name} analysis complete: ${reduction_percentage}% reduction`,
});
}
res.json({ received: true });
});5. Sync to Database
Keep local database in sync:
app.post('/webhook', async (req, res) => {
const { event, data } = req.body;
switch (event) {
case 'project.created':
await db.projects.create(data);
break;
case 'project.deleted':
await db.projects.delete(data.project_id);
break;
case 'analysis.completed':
await db.runs.create(data);
break;
}
res.json({ received: true });
});TROUBLESHOOTING
Webhook Not Receiving Events
Possible causes:
- Endpoint not publicly accessible
- Firewall blocking requests
- Webhook disabled
- No events matching selected types
Solutions:
- Test endpoint with curl
- Check firewall rules
- Verify webhook is enabled
- Review event selection
Signature Verification Failing
Possible causes:
- Wrong secret used
- Payload modified before verification
- Body parsing issues
Solutions:
- Verify you're using correct secret
- Use raw body (not parsed JSON)
- Check HMAC implementation
Webhook Timing Out
Possible causes:
- Endpoint too slow (>10s)
- Blocking on long operations
- Network issues
Solutions:
- Optimize endpoint performance
- Use async processing pattern
- Check network connectivity
- Increase timeout (max 10s)
Duplicate Webhooks
Expected behavior:
- Webhooks may be delivered multiple times
- Implement idempotency (check
idfield) - Use database unique constraint
Not a bug - this is by design for reliability
SECURITY CONSIDERATIONS
HTTPS Required
Security Requirement
All webhook endpoints MUST use HTTPS. We will not send webhooks to HTTP endpoints.
Why?
- Prevents man-in-the-middle attacks
- Protects webhook secret
- Encrypts sensitive data
IP Allowlisting
Optional: Restrict requests to CAPYSQUASH IPs
CAPYSQUASH IP Ranges:
# Contact support@capysquash.dev for current IP rangesFirewall rule example:
# iptables
iptables -A INPUT -p tcp --dport 443 -s <CAPYSQUASH_IP> -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j DROPSecret Rotation
Rotate webhook secrets regularly:
- Generate new secret
- Update your verification code
- Save new secret in CAPYSQUASH
- Old secret invalidated immediately
Recommendation: Rotate quarterly or after security incidents
WEBHOOK LIMITS
| Plan | Webhooks Allowed | Delivery Attempts | Rate Limit |
|---|---|---|---|
| Free | 0 | N/A | N/A |
| Creator | 0 | N/A | N/A |
| Professional | 5 webhooks | 3 retries | 100/hour |
| Agency | 15 webhooks | 5 retries | 500/hour |
| Enterprise | Unlimited | Custom | Custom |
NEXT STEPS
How is this guide?