Reference

Webhooks

What LevelFour posts to your endpoint when a recommendation or an optimization changes state, and how to verify the signature before you act on it.

LevelFour delivers webhooks through Svix. When events occur in your account, LevelFour sends HTTPS POST requests to your registered endpoints with a signed payload.

The snippets below use the LevelFour SDK clients. SDKs covers installing one and building client.

Register an endpoint

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

The call takes:

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

url must be HTTPS. Webhooks in the API reference has the full request and response schema.

Manage endpoints

List endpoints

endpoints = client.webhooks.list()

Delete an endpoint

client.webhooks.delete("ep_123")

Events

EventDescription
recommendation.terms_acceptedSomeone accepted the terms for applying a recommendation
recommendation.acceptedA savings recommendation was accepted
recommendation.rejectedA savings recommendation was rejected
recommendation.rejection_reason_addedA reason was added to a rejected recommendation
recommendation.implementation_method_setSomeone chose how a recommendation is delivered
execution.approval_requestedA member submitted a recommendation for an admin to release
optimization.startedAn automated optimization has started processing
optimization.completedAn automated optimization finished successfully
optimization.failedAn automated optimization failed

Payloads

recommendation.accepted / recommendation.rejected

{
    "recommendation_id": "REC-1234",
    "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"
}

A rejected event carries rejection_reason and rejection_explanation, and its status is rejected.

recommendation.terms_accepted

{
    "recommendation_id": "REC-1234",
    "terms_accepted_by": "user@example.com",
    "terms_accepted_at": "2025-10-15T10:00:00Z"
}

recommendation.rejection_reason_added

{
    "recommendation_id": "REC-1234",
    "saving_acceptance": "rejected",
    "rejection_reason": "not_applicable",
    "rejection_explanation": "This volume backs a disaster recovery test."
}

rejection_reason is one of operational, strategy, not_applicable or other.

recommendation.implementation_method_set

{
    "recommendation_id": "REC-1234",
    "implementation_method": "iac",
    "status": "pending"
}

execution.approval_requested

{
    "recommendation_id": "REC-1234",
    "execution_request_status": "pending_approval",
    "execution_request_by": "user@example.com"
}

Subscribe an endpoint your approvers watch to this event. Without it, a request waits in the dashboard until an admin opens it.

optimization.started

{
    "recommendation_id": "REC-1234",
    "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. l4 rec execute has a table of what each one does.

optimization.completed

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

optimization.failed

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

Verify the signature

Always verify webhook signatures before processing events. Your endpoint is a public URL, so a handler that skips this acts on whatever reaches it.

Every webhook request carries these 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.
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)
Keep the signing secret in an environment variable or a secrets manager, never in a file you commit.

The algorithm

Each signature is an HMAC over the message id, the timestamp and the body, joined by dots:

base64(HMAC-SHA256(base64_decode(secret), "{webhook-id}.{webhook-timestamp}.{body}"))
Every signing secret starts with whsec_. The SDK verifiers strip this prefix before they base64-decode the secret bytes, so pass the value exactly as it was issued.

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)

A wider tolerance widens the window in which a captured request can be replayed.

Retries

Svix retries failed deliveries with exponential backoff. A delivery fails if your endpoint:

  • Returns a non-2xx HTTP status code
  • Does not respond within the timeout
  • Is unreachable
A 2xx is what tells LevelFour the delivery worked. Return it as soon as the signature checks out and the event is stored, then do the heavy processing asynchronously.
A retry replays the same event, so one webhook-id can reach you more than once. Implement idempotent handlers using the webhook-id header for deduplication.

Next

On this page

Ask the FinOps Agent about your cloud spend