Webhooks

Overview

LevelFour uses Svix for webhook delivery. When events occur in your account, LevelFour sends HTTPS POST requests to your registered endpoints with a signed payload.

Registering an Endpoint

endpoint = client.webhooks.register(
    url="https://your-app.com/webhooks/levelfour",
    event_types=["recommendation.accepted", "optimization.completed"],
    description="Production webhook",
)

Request Parameters

ParameterTypeRequiredDescription
urlstringYesHTTPS endpoint URL
event_typesstring[]YesEvents to subscribe to
descriptionstringNoHuman-readable description

Managing Endpoints

List Endpoints

endpoints = client.webhooks.list()

Delete an Endpoint

client.webhooks.delete("ep_123")

Event Types

EventDescription
recommendation.acceptedA savings recommendation was accepted
recommendation.rejectedA savings recommendation was rejected
optimization.startedAn automated optimization has started processing
optimization.completedAn automated optimization finished successfully
optimization.failedAn automated optimization failed

Event Payloads

recommendation.accepted / recommendation.rejected

{
    "recommendation_id": "rec_123",
    "saving_acceptance": "accepted",
    "saving_accepted_by": "user@example.com",
    "saving_accepted_at": "2025-10-15T10:00:00",
    "rejection_reason": null,
    "rejection_explanation": null,
    "status": "optimized"
}

For rejected events, rejection_reason and rejection_explanation will be populated, and status will be rejected.

optimization.started

{
    "recommendation_id": "rec_123",
    "status": "processing",
    "implementation_method": "one-click",
    "completed_at": "2025-10-15T10:05:00Z",
    "message": "Optimization started"
}

The implementation_method field can be: one-click, iac, one-click-plus-iac, or manual.

optimization.completed

{
    "recommendation_id": "rec_123",
    "status": "optimized",
    "implementation_method": "one-click"
}

optimization.failed

{
    "recommendation_id": "rec_123",
    "status": "failed",
    "implementation_method": "one-click"
}

Signature Verification

Every webhook request includes three headers for HMAC-SHA256 signature verification:

HeaderDescription
webhook-idUnique message ID (for deduplication)
webhook-timestampUnix timestamp in seconds
webhook-signaturev1,<base64-encoded-signature>

Legacy Svix headers (svix-id, svix-timestamp, svix-signature) are also supported.

Python

from levelfour.webhooks.verifier import WebhookVerifier, WebhookVerificationError

verifier = WebhookVerifier("whsec_your_signing_secret")

try:
    payload = verifier.verify(
        payload=request_body,
        headers={
            "webhook-id": headers["webhook-id"],
            "webhook-timestamp": headers["webhook-timestamp"],
            "webhook-signature": headers["webhook-signature"],
        },
    )
    handle_event(payload)
except WebhookVerificationError:
    return Response(status_code=400)

TypeScript

import { WebhookVerifier, WebhookVerificationError } from "levelfour";

const verifier = new WebhookVerifier("whsec_your_signing_secret");

try {
    const payload = verifier.verify(requestBody, {
        "webhook-id": headers["webhook-id"],
        "webhook-timestamp": headers["webhook-timestamp"],
        "webhook-signature": headers["webhook-signature"],
    });
    handleEvent(payload);
} catch (err) {
    if (err instanceof WebhookVerificationError) {
        res.status(400).json({ error: "Invalid signature" });
    }
}

Go

import (
    "io"
    "net/http"

    "github.com/LevelFourAI/levelfour-go/levelfour/webhooks"
)

verifier, err := webhooks.NewVerifier("whsec_your_signing_secret")
if err != nil {
    log.Fatal(err)
}

http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "failed to read body", http.StatusBadRequest)
        return
    }

    payload, err := verifier.Verify(r.Header, body)
    if err != nil {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }

    handleEvent(payload)
    w.WriteHeader(http.StatusOK)
})

Signature Algorithm

The signature is computed as:

base64(HMAC-SHA256(base64_decode(secret), "{webhook-id}.{webhook-timestamp}.{body}"))

Signing secrets are prefixed with whsec_. The SDK verifiers strip this prefix automatically before base64-decoding the secret bytes.

Timestamp Tolerance

By default, the verifiers reject messages with timestamps more than 5 minutes from the current time. This prevents replay attacks. You can customize the tolerance:

payload = verifier.verify(body, headers, tolerance_seconds=600)

Retry Behavior

Svix automatically retries failed deliveries using exponential backoff. A delivery is considered failed if your endpoint:

  • Returns a non-2xx HTTP status code
  • Does not respond within the timeout
  • Is unreachable

Implement idempotent handlers using the webhook-id header for deduplication.

Security Best Practices

  • Always verify webhook signatures before processing events
  • Use HTTPS endpoints only
  • Respond with 2xx quickly and do heavy processing asynchronously
  • Store your signing secret securely (environment variable or secrets manager)
  • Implement idempotent handlers since webhooks may be retried on delivery failure