Events & Webhooks

When something notable happens in your ledger, Ledgr records an event. Rather than polling the API to find out what changed, you register a webhook destination, a URL of your own, and Ledgr sends each matching event to it as it happens. Delivery is signed so you can trust it, and retried so a brief outage on your side does not lose an event. Events and destinations belong to your whole account, so these endpoints do not take a program_id header.

What Ledgr sends events for

Event Sent when
transaction.created A transaction is created, whether pending or posted.
transaction.posted A transaction settles.
transaction.reversed A transaction is reversed.
transaction.failed, transaction.canceled A transaction ends without settling.
transaction.disputed A transaction is marked as disputed.
hold.created A hold is placed.
hold.captured A hold is captured, in full or in part.
hold.released A hold is released.

The event payload

Each delivery is a JSON body in this shape:

{
  "id": "evt_c421c1ed-0cbe-4b3b-827c-cf4a74987128",
  "type": "transaction.posted",
  "created": 1786045169,
  "data": {
    "transaction_group_id": "trx-grp_49202f18-7778-4949-8b06-c9d7b4d71e54",
    "amount": 102,
    "currency_code": "USD"
  }
}

Confirming an event is genuine

Anyone can send a POST to your URL, so verify that a delivery really came from Ledgr before you act on it. Every delivery includes a signature header, along with headers naming the event type and delivery id:

X-Ledgr-Signature: t=1786045169,v1=4f0b3f2a...c9d7

The signature is an HMAC-SHA256 of the string "<t>.<raw_body>", keyed with the destination's signing secret. Recompute it over the exact raw request body, compare it to the header with a constant-time check, and reject deliveries whose timestamp is too old so an intercepted request cannot be replayed later. You can view or roll the signing secret on the destination's page.

import crypto from "crypto";

function verify(rawBody: string, header: string, secret: string): boolean {
  const [t, v1] = header.split(",").map((p) => p.split("=")[1]);
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

Delivery and retries

Return a 2xx status to acknowledge a delivery. Any other status, or a timeout, counts as a failure, and Ledgr retries with increasing gaps between attempts over about an hour. Because a retry can arrive after your handler already ran, treat the event id as something you have seen before: record which ids you have processed and ignore repeats. That keeps your handling correct even under retries.

Operations

Method and path What it does
POST /webhooks Create a destination.
GET /webhooks List destinations.
GET /webhook Retrieve one destination, with recent delivery stats.
PUT /webhooks Update a destination, such as enabling it or changing which events it receives.
DELETE /webhooks Delete a destination.
POST /webhooks/roll Issue a new signing secret for a destination.
POST /webhooks/resend Send a delivery again.
GET /webhooks/deliveries List the deliveries made to a destination.
GET /events List events, each with a summary of how it was delivered.
GET /event Retrieve one event with its payload and every delivery attempt.

The API reference documents the parameters and responses for each of these.