Real-time event notifications with HMAC signature verification
Webhooks let you subscribe to LimitGuard events and receive real-time HTTP POST notifications to your server the moment something changes — a trust score update, a new compliance alert, or a certificate issuance. Instead of polling the API, your system reacts instantly.
Webhooks require an API key with the starter tier or above. Sandbox webhooks fire with simulated payloads and do not count against your quota.
Every time an event occurs in the LimitGuard platform, we send an HTTP POST request to each registered endpoint with a JSON payload describing what happened. Your server responds with HTTP 200 to acknowledge receipt. If delivery fails, we retry with exponential backoff.Each request includes an X-LimitGuard-Signature header containing an HMAC-SHA256 signature computed from the raw request body using the webhook secret returned at registration. Always verify this signature before processing the payload.
The secret is returned once only at creation and is never shown again. Store it immediately in a secure secret manager (e.g., AWS Secrets Manager, HashiCorp Vault, or a .env file that is never committed to source control).
2
Verify the signature
Before processing any webhook payload, verify that it genuinely came from LimitGuard by computing the expected HMAC-SHA256 signature and comparing it to the value in the X-LimitGuard-Signature header.
import hashlibimport hmacdef verify_signature(payload: bytes, signature_header: str, secret: str) -> bool: """ Returns True if the payload matches the LimitGuard signature. Always use this before processing any webhook event. """ expected = hmac.new( secret.encode("utf-8"), payload, hashlib.sha256, ).hexdigest() # Use hmac.compare_digest to prevent timing attacks return hmac.compare_digest(f"sha256={expected}", signature_header)# --- FastAPI example ---from fastapi import FastAPI, Request, HTTPExceptionapp = FastAPI()WEBHOOK_SECRET = "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # from env@app.post("/webhooks/limitguard")async def handle_webhook(request: Request): payload = await request.body() signature = request.headers.get("X-LimitGuard-Signature", "") if not verify_signature(payload, signature, WEBHOOK_SECRET): raise HTTPException(status_code=401, detail="Invalid signature") event = await request.json() # Hand off to async worker — respond 200 immediately await process_event(event) return {"received": True}
Always verify signatures using a constant-time comparison (hmac.compare_digest in Python, crypto.timingSafeEqual in Node.js). Standard string equality (===) is vulnerable to timing attacks.
3
Handle the event
Each webhook payload follows a consistent envelope structure. Dispatch on event.type:
New event types may be added as LimitGuard adds data sources. Subscribing to ["*"] is the easiest way to stay current, but ensure your handler ignores unknown types gracefully.
If your endpoint returns anything other than a 2xx status code, or if the connection times out, LimitGuard retries delivery with exponential backoff:
Attempt
Delay
Cumulative Time
1 (initial)
—
0s
2
30s
30s
3
5 min
~5.5 min
4 (final)
30 min
~36 min
After 4 failed attempts, the event is marked as undelivered and no further retries occur. You can replay undelivered events from the dashboard or by sending a test event.Timeout: LimitGuard waits up to 10 seconds for your server to respond. Respond with 200 immediately and process the payload asynchronously.
If your endpoint consistently fails (>10 consecutive failures), the webhook is automatically deactivated to protect system stability. You’ll receive an email notification. Re-enable it from the dashboard or by POSTing to POST /v1/webhooks/{webhook_id}/enable.
Test events include a realistic synthetic payload using the Acme Corp BV sandbox entity. The signature is computed using your real webhook secret, so your full verification stack is exercised.
Run the test endpoint from your CI/CD pipeline after deployment to verify your webhook handler is reachable and returning 200 before going live.
Acknowledge the delivery within 10 seconds by returning 200 OK before doing any meaningful work. Offload processing to a background queue (Celery, BullMQ, SQS, etc.):
Python
# GOOD — respond immediately, process in background@app.post("/webhooks/limitguard")async def handle_webhook(request: Request): payload = await request.body() if not verify_signature(payload, request.headers.get("X-LimitGuard-Signature", ""), WEBHOOK_SECRET): raise HTTPException(status_code=401, detail="Invalid signature") event = await request.json() await queue.enqueue("process_limitguard_event", event) # async, non-blocking return {"received": True} # Return 200 immediately# BAD — slow processing blocks the response@app.post("/webhooks/limitguard")async def handle_webhook_slow(request: Request): event = await request.json() await run_heavy_database_operations(event) # Will time out for slow ops return {"received": True}
LimitGuard may deliver the same event more than once on retry. Use the id field as an idempotency key:
Python
async def process_event(event: dict): event_id = event["id"] # Check if already processed if await db.event_processed(event_id): return # Safe to ignore — already handled # Mark as processed atomically before side effects async with db.transaction(): await db.mark_event_processed(event_id) await apply_business_logic(event)
LimitGuard only delivers to https:// endpoints with a valid TLS certificate. Self-signed certificates and http:// URLs are rejected at registration time.
Error (400 Bad Request)
{ "error": "invalid_webhook_url", "message": "Webhook URL must use HTTPS with a valid certificate."}