Browse documentation

Integrations

Webhook events

Subscribe your own endpoint to Lineman cost events, verify every delivery with an HMAC signature, and re-send anything that failed.

The event envelope

Every event, whatever its type, arrives in the same JSON shape. type comes from the published taxonomy and schema_version tells you which envelope you are looking at. Call GET /v1/event-types for the current list.

{
  "id": "6f1c0e6e-6d1a-4f8e-9a0e-9b7d2f5c1a33",
  "type": "cost_alert.budget_day",
  "schema_version": 1,
  "company_id": "5b0f...",
  "occurred_at": "2026-07-25T09:30:00.000Z",
  "data": {
    "alert_type": "budget_day",
    "title": "Daily budget exceeded",
    "subject_user_id": null,
    "dedup_key": "co1:budget_day::2026-07-25",
    "detail": { "spend_usd_micro": 2500000, "spend_usd": 2.5 }
  }
}

Amounts are micro-USD integers in _usd_micro fields, each with a decimal _usd sibling alongside it. Times are ISO-8601 UTC.

De-duplicate on the event id

Treat id as an idempotency key and ignore an id you have already handled. This is not optional: the same event can reach you more than once, by design.

  • Retries. A failed delivery is retried with backoff, and every retry carries the same id.
  • Replays. Re-sending a delivery from the log sends the original body again, unchanged, so the id is identical. Only the timestamp and the signature are fresh.
  • Several endpoints. One event delivered to two of your endpoints is one event with one id, not two events.

Verify the signature

Each request carries X-Lineman-Timestamp in Unix seconds and X-Lineman-Signature, which is sha256= followed by the HMAC-SHA256 of the literal string timestamp.body under your signing secret. Sign the raw body, before any JSON parsing.

const expected =
  "sha256=" +
  crypto
    .createHmac("sha256", process.env.LINEMAN_WEBHOOK_SECRET)
    .update(timestampHeader + "." + rawBody)
    .digest("hex");

// Reject anything older than five minutes, then compare in constant time.
if (Math.abs(Date.now() / 1000 - Number(timestampHeader)) > 300) return reject();
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader)))
  return reject();

During a secret rotation a second header, X-Lineman-Signature-Previous, carries the same body signed with your previous secret. Accept either one while you roll the new secret out.

Add an endpoint

An endpoint is a URL, a signing secret and a list of event types. Leave the list empty to receive every type, including ones published later. Only a company administrator can add one, because an endpoint receives the whole company's cost events.

  1. 1Pick the events you want from GET /v1/event-types.
  2. 2Create the endpoint with POST /v1/webhooks/endpoints, passing an https URL and a signing secret of 16 to 256 characters.
  3. 3Send yourself a test with POST /v1/webhooks/endpoints/:id/test and confirm your signature check passes.
  4. 4Watch the first real deliveries land in GET /v1/webhooks/deliveries.
curl -X POST https://api.lineman.io/v1/webhooks/endpoints \
  -H "Authorization: Bearer $LINEMAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://hooks.example.com/lineman",
        "label": "ops service",
        "event_types": "cost_alert.budget_day,cost_alert.budget_month",
        "secret": "a-long-random-signing-secret"
      }'

When a delivery fails

Every attempt is recorded, including what your endpoint answered. Nothing is dropped quietly.

  • Retried after roughly 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, for a 5xx, a 408, a 429 or a network error.
  • Given up on straight away for any other 4xx, since re-sending the same body would fail the same way, and after the final retry otherwise. The row is stamped with gave_up_at.
  • Readable at GET /v1/webhooks/deliveries, with the status code and a bounded snippet of your endpoint's response.
  • Re-sendable with POST /v1/webhooks/deliveries/:id/replay once you have fixed the receiver.

Rotate the signing secret without downtime

Rotating a secret used to mean the old one stopped verifying the instant the new one was saved, so your deployment had to land at the same second. It no longer does.

  1. 1Call PUT /v1/webhooks/endpoints/:id/secret with the new secret. The previous secret stays valid for 24 hours by default, or for whatever you pass as overlap_seconds, up to seven days.
  2. 2During the overlap, verify against either X-Lineman-Signature or X-Lineman-Signature-Previous. Deploy the new secret whenever suits you.
  3. 3There is nothing to do at the end: the previous secret expires on its own. Pass overlap_seconds of 0 if you would rather cut over instantly.

Who can do what

Reading endpoints and the delivery log needs an administrator or an auditor. Adding, changing, testing, rotating and replaying all need an administrator.

The single alert webhook configured under Cost Explorer settings still works exactly as before and is untouched. Endpoints here are separate, so adding one does not duplicate anything you already receive.