CAPYSQUASH

---

CAPYSQUASH documentation page.

---

Title: Webhooks. Description: 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

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 });

});

</Tab>

<Tab value="Python">
  ```python

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(), 401

event = request.json
print(f"Webhook received: ")

return jsonify()




  ```go
  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}`))

}

</Tab>

<Tab value="Ruby">
  ```ruby
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 .to_json

end

event = JSON.parse(payload)

puts "Webhook received: #"

.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 (&lt;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:**

```json
{
"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

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