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:
- HMAC-SHA256 signed payloads for security
- Automatic retries (3 attempts with exponential backoff)
- Per-escalation or wildcard subscriptions
- Delivery tracking for debugging
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:
url(required) — Your webhook receiver endpoint (must be HTTPS in production)events(required) — Array of event types to subscribe to (use["*"]for all events)escalation_id(optional) — Specific escalation ID to watch (omit ornullfor wildcard)secret(required) — Shared secret for HMAC-SHA256 signature (min 32 characters)
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:
Content-Type: application/jsonX-GaaS-Signature: sha256=<hmac_signature>— HMAC-SHA256 signature for verificationX-GaaS-Event: <event_type>— Event type (e.g.,completed)X-GaaS-Webhook-ID: wh_abc123...— Webhook ID that triggered this delivery
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();
}
}
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:
- Attempt 1: Immediate delivery
- Attempt 2: After 5 seconds
- Attempt 3: After 25 seconds (exponential backoff)
After 3 failed attempts, the delivery is marked as failed. You can view delivery status via GET /v1/escalations/webhooks/{webhook_id}/deliveries.
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
- Visit webhook.site
- Copy your unique URL (e.g.,
https://webhook.site/abc-123-def) - Register that URL as a webhook in GaaS
- 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
- Always verify signatures to prevent spoofing and replay attacks.
- Use HTTPS for webhook URLs in production (required for security).
- Respond quickly (within 5 seconds) to avoid timeouts. Use async processing for long-running tasks.
- Make webhook handlers idempotent — you may receive duplicate events due to retries.
- Store webhook secrets securely (environment variables or secrets manager, never in code).
- Monitor delivery failures and alert on repeated failures (indicates endpoint downtime).
- Use wildcard subscriptions (
["*"]) to receive all events, then filter on your end.
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
- Getting Started — Onboard and submit your first intent
- Authentication — API key setup and security
- API Reference — Complete endpoint documentation