CostLensWebhooks

Webhooks

Get an HTTP POST the moment a recommendation is approved, an anomaly is acknowledged, or a fix completes — instead of polling the public API.

Updated September 20263 min read

Webhooks push events to your own endpoint as they happen, so your system can react to a recommendation being approved, an anomaly being acknowledged, or a fix completing, failing, or rolling back — without polling /public/v1.

Connecting a webhook

  1. Go to Settings → API Access (organization admins only) and find the Event webhooks card.
  2. Click Create webhook, enter your endpoint's HTTPS URL, and choose which events to subscribe to.
  3. CostLens generates a signing secret and shows it to you once, at creation — save it now. It's used to verify that a delivery genuinely came from CostLens.

Your endpoint, your responsibility

CostLens validates that the URL resolves to a public address (no localhost, no private/internal IP ranges) but otherwise delivers to whatever endpoint you configure. Make sure it's reachable, fast (a slow endpoint delays retries for everyone sharing the delivery queue), and — critically — that you verify the signature below before trusting a payload.

Event catalog

EventFires when
recommendation_approvedA recommendation is approved (via the app, the public API, or Auto-Fix)
recommendation_dismissedA recommendation is dismissed
anomaly_acknowledgedAn anomaly is acknowledged
fix_appliedA fix completes successfully
fix_failedA fix fails to apply
fix_rolled_backA completed fix is rolled back

Payload shape

Every event delivers the same envelope, regardless of type:

json
{
  "event_type": "recommendation_approved",
  "sent_at": "2026-09-24T10:15:00Z",
  "title": "Recommendation approved",
  "summary": "ec2_rightsizing on i-0abc123 was approved",
  "severity": "info",
  "facts": {
    "Check": "ec2_rightsizing",
    "Resource": "i-0abc123",
    "Estimated savings": "612.40"
  }
}

This is a neutral summary, not a raw dump of the underlying resource — the same shape CostLens's Slack/Teams integrations render from. If you need the full resource, follow up with a GET against the relevant /public/v1 endpoint using the ids in facts.

Verifying a delivery

Every request carries an X-CostLens-Signature header shaped like Stripe's well-documented scheme — a timestamp bound into the signed material, not just alongside it, so a captured request can't be replayed indefinitely:

X-CostLens-Signature: t=1735689600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Verify it by recomputing the HMAC-SHA256 over "{t}.{raw_request_body}" using your signing secret, and comparing in constant time:

ts
import crypto from "node:crypto";
 
function verify(secret: string, rawBody: string, header: string, toleranceSeconds = 300): boolean {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=") as [string, string]));
  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
 
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
 
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}
python
import hashlib
import hmac
import time
 
def verify(secret: str, raw_body: str, header: str, tolerance_seconds: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    timestamp = int(parts.get("t", 0))
    if not timestamp or abs(time.time() - timestamp) > tolerance_seconds:
        return False
 
    expected = hmac.new(
        secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Always compute the signature over the raw request body — before any JSON parsing — since re-serializing can change field order or whitespace and produce a different byte string than what was actually signed.

Retries and dead-lettering

A delivery that doesn't get a 2xx response is retried with backoff — 1 minute, 5 minutes, 30 minutes, 2 hours, then 12 hours — for up to 6 total attempts before it's marked dead-lettered. You can view every delivery attempt (status, HTTP code, timestamp) on the webhook's card in Settings → API Access, and manually replay any failed or dead-lettered delivery once your endpoint is back up — it resends the exact original signed payload, not a freshly-built one.

Rotating your signing secret

Click Rotate secret on the webhook's card to generate a new one — shown once, just like at creation. It takes effect immediately for all future deliveries; there's no overlap window, since a signing secret only ever verifies inbound deliveries; nothing is "mid-flight" the way an API key's holder might be.

Managing a webhook

From the webhook's card you can toggle it on/off, edit which events it's subscribed to, rotate its secret, or delete it entirely — deleting stops all future deliveries immediately and can't be undone.

CostLens
Previous
SDKs
CostLens
Next
Public API Changelog