Skip to main content
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.

How It Works

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.

Setup

1

Register your endpoint

Send a POST /v1/webhooks request with your HTTPS URL and the event types you want to receive.
curl -X POST https://api.limitguard.ai/v1/webhooks \
  -H "X-API-Key: lg_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/limitguard",
    "events": ["entity.checked", "score.updated", "compliance.alert"]
  }'
Response (201 Created)
{
  "webhook_id": "wh_abc123def456",
  "url": "https://your-app.com/webhooks/limitguard",
  "events": ["entity.checked", "score.updated", "compliance.alert"],
  "secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
  "status": "active",
  "created_at": "2026-02-20T10:00:00Z"
}
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 hashlib
import hmac

def 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, HTTPException

app = 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:
Payload Envelope
{
  "id": "evt_01JMVQX3KZPB4Y8N9C2WD7FGH1",
  "type": "score.updated",
  "api_version": "2026-02-01",
  "created_at": "2026-02-20T14:32:07.419Z",
  "livemode": true,
  "data": {
    "entity_id": "ent_acme_corp_bv_nl",
    "entity_name": "Acme Corp BV",
    "country": "NL",
    "previous_score": 74,
    "new_score": 87,
    "trust_level": "high",
    "recommendation": "proceed",
    "changed_sources": ["kvk", "sanctions"],
    "processing_time_ms": 312
  }
}
async def process_event(event: dict):
    event_type = event["type"]
    data = event["data"]

    if event_type == "entity.checked":
        await on_entity_checked(data)
    elif event_type == "score.updated":
        await on_score_updated(data)
    elif event_type == "compliance.alert":
        await on_compliance_alert(data)
    elif event_type == "certificate.issued":
        await on_certificate_issued(data)
    elif event_type == "certificate.revoked":
        await on_certificate_revoked(data)
    else:
        # Forward-compatible: ignore unknown event types
        pass
Use the event id field (evt_...) as an idempotency key. Store processed event IDs in your database to safely deduplicate retried deliveries.

Event Types

EventTriggerKey Payload Fields
entity.checkedAny POST /v1/entity/check call completesentity_id, trust_score, trust_level, recommendation, sources_checked
score.updatedA monitored entity’s trust score changes by ≥5 pointsentity_id, previous_score, new_score, changed_sources
compliance.alertNew sanctions match, FATF listing, or adverse media flagentity_id, alert_type, severity, source, description
certificate.issuedA new trust certificate is generated for an entityentity_id, certificate_id, valid_until, score_at_issuance
certificate.revokedA trust certificate is revoked (score dropped or entity flagged)entity_id, certificate_id, revocation_reason

Subscribe to All Events

Pass "events": ["*"] to receive every event type, including future additions.
{
  "url": "https://your-app.com/webhooks/limitguard",
  "events": ["*"]
}
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.

Signature Verification

Every webhook request includes two headers:
HeaderExample ValueDescription
X-LimitGuard-Signaturesha256=a3f8c...HMAC-SHA256 of the raw request body
X-LimitGuard-Eventscore.updatedThe event type (convenience header)
X-LimitGuard-Deliveryevt_01JMVQX3KZ...Unique delivery ID for deduplication
X-LimitGuard-Timestamp1740054727Unix timestamp of delivery attempt

Computing the Signature

HMAC-SHA256(key=webhook_secret, message=raw_request_body)
The X-LimitGuard-Signature header value is formatted as sha256=<hex_digest>.

Replay Attack Prevention

Compare the X-LimitGuard-Timestamp header against your server’s current time. Reject deliveries older than 5 minutes to prevent replay attacks:
Python
import time

def verify_webhook(payload: bytes, headers: dict, secret: str) -> bool:
    timestamp = int(headers.get("X-LimitGuard-Timestamp", "0"))
    signature = headers.get("X-LimitGuard-Signature", "")

    # Reject stale deliveries (older than 5 minutes)
    if abs(time.time() - timestamp) > 300:
        return False

    expected = f"sha256={hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()}"
    return hmac.compare_digest(expected, signature)

Retry Logic

If your endpoint returns anything other than a 2xx status code, or if the connection times out, LimitGuard retries delivery with exponential backoff:
AttemptDelayCumulative Time
1 (initial)0s
230s30s
35 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.

Managing Webhooks

List Active Webhooks

curl -X GET https://api.limitguard.ai/v1/webhooks \
  -H "X-API-Key: lg_live_xxxxxxxxxxxxxxxxxxxx"

Remove a Webhook

curl
curl -X DELETE https://api.limitguard.ai/v1/webhooks/wh_abc123def456 \
  -H "X-API-Key: lg_live_xxxxxxxxxxxxxxxxxxxx"
Returns 204 No Content on success. The endpoint immediately stops receiving deliveries.

Testing

Use the test endpoint to fire a simulated event to your webhook without triggering a real check:
curl -X POST https://api.limitguard.ai/v1/webhooks/wh_abc123def456/test \
  -H "X-API-Key: lg_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"event": "entity.checked"}'
Response
{
  "delivery_id": "evt_test_01JMVR2KXPB4Y8N9C2WD7FGH1",
  "event": "entity.checked",
  "status": "delivered",
  "response_code": 200,
  "response_time_ms": 43,
  "delivered_at": "2026-02-20T14:32:07.419Z"
}
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.

Best Practices

Respond Immediately, Process Asynchronously

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}

Implement Idempotency

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)

Forward-Compatible Event Handling

New event types will be introduced as LimitGuard adds capabilities. Always handle unknown types gracefully rather than raising an exception:
Python
KNOWN_EVENTS = {"entity.checked", "score.updated", "compliance.alert",
                "certificate.issued", "certificate.revoked"}

async def process_event(event: dict):
    if event["type"] not in KNOWN_EVENTS:
        logger.info(f"Ignoring unknown event type: {event['type']}")
        return  # Do not raise — return 200 to prevent retries

Validate Event Data

Even after signature verification, validate required fields before using them:
Python
def validate_score_updated(data: dict) -> bool:
    required = {"entity_id", "previous_score", "new_score", "trust_level"}
    return required.issubset(data.keys())

Use HTTPS with a Valid Certificate

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."
}

Webhook Payload Reference

All event payloads share the same outer envelope:
FieldTypeDescription
idstringGlobally unique event ID (evt_...) — use as idempotency key
typestringEvent type (e.g., score.updated)
api_versionstringAPI version at time of delivery (e.g., 2026-02-01)
created_atstringISO 8601 timestamp of the event
livemodebooleanfalse for sandbox events
dataobjectEvent-specific payload (varies by type)

Next Steps