Webhooks

Real-time event notifications with HMAC-SHA256 signature verification.

Overview

Webhooks allow you to receive real-time notifications when escalations change state. Instead of polling for updates, GaaS sends an HTTP POST request to your server with event details whenever an escalation is created, claimed, reviewed, reassigned, or cancelled.

Key features:


Available Events

GaaS sends webhook notifications for the following escalation lifecycle events:

Event Type Description When Triggered
created Escalation created Governance decision requires human review (ESCALATE verdict)
claimed Escalation claimed by a reviewer Reviewer assigns themselves to the escalation
completed Escalation resolved (approved or rejected) Reviewer submits final decision
rejected Escalation rejected by reviewer Reviewer blocks the action
modified Escalation approved with modifications Reviewer approves with payload changes
reassigned Escalation reassigned to different reviewers Admin changes assigned reviewers
cancelled Escalation cancelled Admin cancels escalation (no longer needed)
timeout Escalation timed out No review received within configured SLA window

Registering a Webhook

Create a webhook subscription with a POST request to the webhooks endpoint:

POST https://api.gaas.is/v1/escalations/webhooks
X-API-Key: your_api_key
Content-Type: application/json

{
  "url": "https://yourapp.com/webhooks/gaas",
  "events": ["created", "completed", "timeout"],
  "escalation_id": null,
  "secret": "your_webhook_secret_32_chars_min"
}

Parameters:

The response includes a webhook ID:

{
  "webhook_id": "wh_abc123...",
  "url": "https://yourapp.com/webhooks/gaas",
  "events": ["created", "completed", "timeout"],
  "created_at": "2026-02-13T10:30:00Z"
}

Event Payload Structure

When an event occurs, GaaS sends a POST request to your webhook URL with this structure:

{
  "event": "completed",
  "escalation_id": "esc_abc123...",
  "timestamp": "2026-02-13T10:35:00Z",
  "data": {
    "escalation_id": "esc_abc123...",
    "status": "resolved",
    "reviewer": "reviewer@company.com",
    "decision": "approve",
    "reasoning": "Action complies with policy, approved.",
    "audit_record_id": "aud_xyz789..."
  }
}

Headers sent with webhook:


Signature Verification (HMAC-SHA256)

Always verify webhook signatures to ensure requests are from GaaS and haven't been tampered with. The X-GaaS-Signature header contains an HMAC-SHA256 signature of the raw request body.

Python (Flask)

import hashlib
import hmac
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_32_chars_min"

@app.route("/webhooks/gaas", methods=["POST"])
def handle_webhook():
    # Get signature from header
    signature_header = request.headers.get("X-GaaS-Signature")
    if not signature_header or not signature_header.startswith("sha256="):
        abort(401, "Missing or invalid signature")

    expected_sig = signature_header[7:]  # Remove "sha256=" prefix

    # Compute HMAC-SHA256 of raw request body
    raw_body = request.get_data()
    computed_sig = hmac.new(
        WEBHOOK_SECRET.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison to prevent timing attacks
    if not hmac.compare_digest(expected_sig, computed_sig):
        abort(401, "Invalid signature")

    # Signature valid, process event
    payload = request.get_json()
    event_type = payload["event"]
    escalation_id = payload["escalation_id"]

    if event_type == "created":
        handle_escalation_created(payload)
    elif event_type == "completed":
        handle_escalation_completed(payload)

    return {"status": "received"}, 200

TypeScript (Express)

import express from 'express';
import crypto from 'crypto';

const app = express();
const WEBHOOK_SECRET = 'your_webhook_secret_32_chars_min';

// Important: Use raw body for signature verification
app.use('/webhooks/gaas', express.raw({ type: 'application/json' }));

app.post('/webhooks/gaas', (req, res) => {
  const signatureHeader = req.headers['x-gaas-signature'];
  if (!signatureHeader || !signatureHeader.startsWith('sha256=')) {
    return res.status(401).send('Missing or invalid signature');
  }

  const expectedSig = signatureHeader.slice(7); // Remove "sha256=" prefix

  // Compute HMAC-SHA256 of raw body
  const computedSig = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  // Constant-time comparison
  if (!crypto.timingSafeEqual(Buffer.from(expectedSig), Buffer.from(computedSig))) {
    return res.status(401).send('Invalid signature');
  }

  // Signature valid, parse and process event
  const payload = JSON.parse(req.body.toString());
  const eventType = payload.event;

  if (eventType === 'created') {
    handleEscalationCreated(payload);
  } else if (eventType === 'completed') {
    handleEscalationCompleted(payload);
  }

  res.json({ status: 'received' });
});

Java (Spring Boot)

import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

@RestController
public class WebhookController {
    private static final String WEBHOOK_SECRET = "your_webhook_secret_32_chars_min";

    @PostMapping("/webhooks/gaas")
    public Map<String, String> handleWebhook(
        @RequestHeader("X-GaaS-Signature") String signatureHeader,
        @RequestBody String rawBody
    ) throws Exception {
        if (signatureHeader == null || !signatureHeader.startsWith("sha256=")) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid signature");
        }

        String expectedSig = signatureHeader.substring(7); // Remove "sha256="

        // Compute HMAC-SHA256
        Mac hmac = Mac.getInstance("HmacSHA256");
        SecretKeySpec key = new SecretKeySpec(
            WEBHOOK_SECRET.getBytes(StandardCharsets.UTF_8),
            "HmacSHA256"
        );
        hmac.init(key);
        byte[] hash = hmac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
        String computedSig = bytesToHex(hash);

        // Constant-time comparison
        if (!MessageDigest.isEqual(expectedSig.getBytes(), computedSig.getBytes())) {
            throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid signature");
        }

        // Process event
        ObjectMapper mapper = new ObjectMapper();
        Map<String, Object> payload = mapper.readValue(rawBody, Map.class);
        String eventType = (String) payload.get("event");

        return Map.of("status", "received");
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}
Security Note: Always use constant-time comparison functions (hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js, MessageDigest.isEqual in Java) to prevent timing attacks when comparing signatures.

Retry Policy

If your webhook endpoint returns a non-2xx status code or times out, GaaS automatically retries delivery:

After 3 failed attempts, the delivery is marked as failed. You can view delivery status via GET /v1/escalations/webhooks/{webhook_id}/deliveries.

Webhook endpoint requirements: Your endpoint must respond within 5 seconds and return a 2xx status code (typically 200 OK). For long-running tasks, acknowledge the webhook immediately and process asynchronously (queue-based processing recommended).

Testing Webhooks

For local development, use ngrok or webhook.site to expose your localhost to the internet:

Using ngrok

# Start ngrok tunnel
ngrok http 3000

# Register webhook with ngrok URL
curl -X POST https://api.gaas.is/v1/escalations/webhooks \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.io/webhooks/gaas",
    "events": ["*"],
    "secret": "test_secret_32_chars_minimum_len"
  }'

Using webhook.site

  1. Visit webhook.site
  2. Copy your unique URL (e.g., https://webhook.site/abc-123-def)
  3. Register that URL as a webhook in GaaS
  4. View payloads and headers in real-time on the webhook.site dashboard

Managing Webhooks

List Webhooks

GET https://api.gaas.is/v1/escalations/webhooks
X-API-Key: your_api_key

Delete Webhook

DELETE https://api.gaas.is/v1/escalations/webhooks/{webhook_id}
X-API-Key: your_api_key

View Delivery History

GET https://api.gaas.is/v1/escalations/webhooks/{webhook_id}/deliveries
X-API-Key: your_api_key

Best Practices


Troubleshooting

Webhooks Not Delivered

Cause: Endpoint unreachable, firewall blocking, or invalid URL.

Solution: Check delivery history for error details. Verify your endpoint is publicly accessible (test with curl). Check firewall/security group rules.

Signature Verification Failing

Cause: Wrong secret, modified body before verification, or encoding mismatch.

Solution: Ensure you're using the raw request body (not parsed JSON). Verify the secret matches exactly. Use UTF-8 encoding.

Duplicate Events

Cause: Webhook retries after timeout or transient failure.

Solution: Make your webhook handler idempotent. Use escalation_id + event + timestamp as a deduplication key.


Related Pages