CAPYSQUASH

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

1

CREATE ENDPOINT

Set up an HTTP endpoint on your server to receive webhooks

2

REGISTER WEBHOOK

  • ► Dashboard → Settings → Webhooks
  • ► Click "Add Webhook"
  • ► Enter your endpoint URL
  • ► Select events to receive
3

VERIFY SIGNATURE

Save the webhook secret and verify signatures in your code

4

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:

  1. We create a signature using your webhook secret
  2. Signature is sent in X-CAPYSQUASH-Signature header
  3. You compute expected signature and compare
  4. 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
end

WEBHOOK 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/json

Timeout

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:

  1. Go to Webhooks → Your webhook
  2. Click "Send Test Event"
  3. Select event type
  4. Click "Send"
  5. 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 id field)
  • 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 ranges

Firewall rule example:

# iptables
iptables -A INPUT -p tcp --dport 443 -s <CAPYSQUASH_IP> -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j DROP

Secret Rotation

Rotate webhook secrets regularly:

  1. Generate new secret
  2. Update your verification code
  3. Save new secret in CAPYSQUASH
  4. Old secret invalidated immediately

Recommendation: Rotate quarterly or after security incidents

WEBHOOK LIMITS

PlanWebhooks AllowedDelivery AttemptsRate Limit
Free0N/AN/A
Creator0N/AN/A
Professional5 webhooks3 retries100/hour
Agency15 webhooks5 retries500/hour
EnterpriseUnlimitedCustomCustom

NEXT STEPS

How is this guide?

On this page