Inspect event deliveries

List, filter, and retry event deliveries for a bridge or effect.

An event delivery is one attempt (and every retry of that attempt) to send a record to a bridge or a webhook. Deliveries come from two sources:

  • Intent messages going through the DTC flow (the ledger's two-phase commit: prepare, commit, abort, status). These always target a bridge — the bridge that owns one of the wallets involved in the intent.
  • Effect events triggered by a signal (e.g. intent-created, anchor-created) — the event types the ledger emits. These target whatever the effect is configured to send to, which is either a bridge or a webhook URL.

Each delivery has a stable handle that doesn't change across retries. The ledger records every transition with a signed proof.

This API is in alpha. Field names, filterable paths, and retry payload shape may still change. It is gated behind two feature flags: events.track-delivery.by-thread and events.track-delivery.by-ledger — both must be on for tracking to fire. See the v2.41.0 release notes for more.

List deliveries

GET /v2/bridges/:handle/events
GET /v2/effects/:handle/events

Also available in the CLI as minka bridge events list and minka effect events list. Full request/response schemas live in the Bridge and Effect API references.

Each record returned has:

PathDescription
data.handleDelivery handle. Stable across retries. Use this to retry a specific delivery.
data.bridgeSet when the delivery targets a bridge (both DTC messages and bridge-targeted effect events).
data.effectSet when the delivery carries an effect event (regardless of whether that effect targets a bridge or a webhook).
data.record + data.linkedRecord type and handle that triggered the delivery (e.g. intent + <handle>).
meta.statusOne of pending, delivered, failed, cancelled. See Statuses.
meta.replayNumber of completed attempts so far.
meta.outputThe same signed object that was sent on the wire. Replayed unchanged on every retry. See Verify the wire payload.
meta.proofsSigned record of every lifecycle transition. See Proofs and history.

Sample response — one delivery from an effect listening on the anchor-created signal. meta.output is truncated for brevity; the full object is the body sent when delivering the event:

{
  "hash": "5b442a9ec54a3f0e4dc892f3afc76c1db604965392ff8e69acf582071aeecf58",
  "meta": {
    "moment": "2026-04-02T05:10:35.123Z",
    "proofs": [
      {
        "signer": "system",
        "method": "ed25519-v2",
        "public": "bctQzN7mjMUNBIx4aSC8WYn03GJWoJjL/KrDb38oU5c=",
        "digest": "2279cd1a2247336592f02a49402dc8eeaaf57a408c7bc79c62fa0f27cac8e4df",
        "result": "q/6jq2yRLm…wQPs0gBQ==",
        "custom": { "moment": "2026-04-02T05:10:35.123Z" }
      }
    ]
  },
  "data": [
    {
      "luid": "$evd.-2DeSFaBtrD8P3-lB",
      "hash": "6d9753379eabde4a1b871b6c398a4115a8f483fed68f0b71183619c9cb9d0bc1",
      "data": {
        "handle": "03fJXUjxygQGvLYEj",
        "bridge": "tesla-bank-bridge",
        "effect": "audit-webhook",
        "record": "anchor",
        "linked": "anchor-001"
      },
      "meta": {
        "status": "delivered",
        "replay": 1,
        "moment": "2026-04-02T05:10:31.535Z",
        "output": { "...": "signed LedgerRecord<LedgerEvent>" },
        "proofs": [
          {
            "method": "ed25519-v2",
            "public": "nDR/rVyWpjLpr5wQL/hB2HZAxjQmJOniqPxRhiNQrmI=",
            "digest": "b91055e14a641501c2aff53a526d8a1cdc76e4613b251df9f83a61c605060767",
            "result": "kQA1g87Y…+w+VFMwdani4LCcQlXVmNCA==",
            "custom": { "moment": "2026-04-02T05:10:30.451Z", "status": "pending" }
          },
          {
            "method": "ed25519-v2",
            "public": "nDR/rVyWpjLpr5wQL/hB2HZAxjQmJOniqPxRhiNQrmI=",
            "digest": "fde3cdad9f2c1e0148598e149a8bc791594814f68b6649cf35a6cf5f5e201f22",
            "result": "Ur/FVpFP…JDKK6i3eYdDQ==",
            "custom": { "moment": "2026-04-02T05:10:31.619Z", "status": "running" }
          },
          {
            "method": "ed25519-v2",
            "public": "nDR/rVyWpjLpr5wQL/hB2HZAxjQmJOniqPxRhiNQrmI=",
            "digest": "66784d7f7ce775007283281cec54a836ae553b32ff6261c5d54a243c7630b9d1",
            "result": "4AaoehruuX…hkLCDA==",
            "custom": {
              "moment": "2026-04-02T05:10:31.653Z",
              "status": "delivered",
              "detail": { "httpStatus": 202 }
            }
          }
        ]
      }
    }
  ],
  "page": { "index": 0, "limit": 10 }
}

Statuses

The ledger keeps retrying a failed delivery with exponential backoff. By default it retries indefinitely; the only thing that stops retries automatically is the target responding with HTTP 501 Not Implemented, which is treated as a permanent failure and moves the delivery to cancelled. The retry cap is also server-configurable per delivery type — when a configured cap is reached the delivery moves to cancelled as well. Filter by meta.status=cancelled to list every delivery the ledger has given up on.

A single webhook call times out after 60 seconds by default (server-configurable); a timeout counts as a failed attempt and is retried per the same rules.

StatusMeaning
pendingAn attempt is scheduled. The ledger will call the target soon, or is waiting between retries.
runningThe ledger is actively calling the target — the HTTP request is in flight. Transient: flips to delivered/failed (or cancelled) as soon as the call settles. You'll only see this if you poll during the call.
deliveredThe target acknowledged with a 2XX response. Terminal.
failedThe most recent attempt returned a non-2XX response, a network error, or timed out. The ledger is still retrying automatically — the status flips back to pending when the next attempt is scheduled, or on to cancelled if retries are exhausted.
cancelledThe ledger has permanently stopped retrying — either the retry cap was reached or the target returned 501 Not Implemented. Terminal until re-queued via the retry endpoint.

Filter

Pass any queries filter via the filter parameter. The most useful paths today:

PathPurpose
meta.statusFind failed/delivered/pending deliveries.
data.record + data.linkedAll deliveries tied to a specific record (e.g. one intent).
meta.output.data.signalWhich event signal triggered the delivery (effect deliveries only).

Every delivery tied to one intent, across DTC bridge messages and any effect events for that intent:

GET /v2/bridges/my-bridge/events?filter={"data.record":"intent","data.linked":"my-intent"}

Deliveries that need attention — last attempt failed (still being retried), or terminal failure (cancelled):

GET /v2/effects/my-effect/events?filter={"meta.status.$in":["failed","cancelled"]}

Retry

POST /v2/bridges/:handle/events/retry
POST /v2/effects/:handle/events/retry

Also available in the CLI as minka bridge events retry and minka effect events retry. Full request/response schemas live in the Bridge and Effect API references.

The body is a signed LedgerRecord — the ledger's standard signed-envelope shape. data carries the retry parameters, hash is the digest of the canonicalised data object (computed by createHash from @minka/crypto, also exposed via the SDK's .hash() builder step), and meta.proofs carries one or more authenticating proofs.

Retry one specific delivery — pass the data.handle you got from the list call:

{
  "hash": "60ea0265e82e7a79073e4d94f74f3faf025a34ab2f0d1dad81ba406fb216280d",
  "data": {
    "handle": "550e8400-e29b-41d4-a716-446655440000"
  },
  "meta": {
    "proofs": [
      {
        "method": "ed25519-v2",
        "public": "gef6OID0o7ZFGTXutV62mh+zv5kgkFP3QLiR+N7syck=",
        "digest": "60ea0265e82e7a79073e4d94f74f3faf025a34ab2f0d1dad81ba406fb216280d",
        "result": "9zamwvKZc4xnj9hFNGz8vwrmdicq+YH3PAQFMJApC0jo9L6ZkkZq3j5bJQmgC8jwLfmIErrP3JNdRKmcP9CpDQ==",
        "custom": {
          "moment": "2025-04-02T05:10:31.535Z"
        }
      }
    ]
  }
}

Bulk-retry every failed delivery for the bridge/effect whose last attempt was within the given window (minutes):

{
  "hash": "f760c5a0ca76979bb16fba1bf1a641875ff8d04d02e7d932bfda9a29c55a0714",
  "data": {
    "maxAge": 60
  },
  "meta": {
    "proofs": [
      {
        "method": "ed25519-v2",
        "public": "dsZvr0rEw9sIffHlv1VP65x1NB8GeXezIv6HONk1SIk=",
        "digest": "f760c5a0ca76979bb16fba1bf1a641875ff8d04d02e7d932bfda9a29c55a0714",
        "result": "0mWjR+NfUBxP4x+/qAn6haDd/31T2N/3zhugUZSn27ESjh/SI6pMEC2FDqGJEGKmDb4D/x/uFQKMSPX2No7BCA==",
        "custom": {
          "moment": "2025-04-05T14:30:30.000Z"
        }
      }
    ]
  }
}

When both are set, data.handle wins and data.maxAge is ignored.

The SDK builds the same envelope for you — .hash() computes the digest from data, .sign([{ keyPair }]) appends the proof, and .send() issues the request. The two shapes above translate to:

import { LedgerSdk } from '@minka/ledger-sdk'

const sdk = new LedgerSdk({ server, ledger })

// Retry one specific delivery.
await sdk.bridge
  .with('my-bridge')
  .events.retry({ handle: '550e8400-e29b-41d4-a716-446655440000' })
  .hash()
  .sign([{ keyPair }])
  .send()

// Bulk-retry every failed delivery within the last hour.
await sdk.bridge
  .with('my-bridge')
  .events.retry({ maxAge: 60 })
  .hash()
  .sign([{ keyPair }])
  .send()

Swap sdk.bridge for sdk.effect to retry effect deliveries instead.

Error cases and edge behaviour

  • Unknown handle. Retrying a handle that doesn't belong to the given bridge or effect — or doesn't exist at all — returns record-not-found (HTTP 404).
  • Current status isn't checked. The ledger reschedules the delivery regardless of whether it is currently cancelled, failed, delivered, or pending. Retrying a delivered delivery will cause the target to receive the same payload a second time — useful for re-playing a message that was lost downstream, but use with care.
  • Bulk retry window is clamped server-side. maxAge is in minutes; the ledger caps it at 48 hours and defaults to 60 minutes when omitted. Values beyond the cap are silently lowered.

Example: retry a failed webhook

You're alerted that a webhook has been failing. Here's the end-to-end flow to investigate, retry, and confirm.

Find the failed delivery and inspect it

Filter on both failed (last attempt failed, still being retried) and cancelled (terminal — retries given up). A purely failed filter would miss deliveries that have already hit cancelled, which is the state most customer-reported issues are in.

CLI:

minka effect events list my-effect --filter '{"meta.status.$in":["failed","cancelled"]}' --verbose

SDK:

const { response } = await sdk.effect
  .with('my-effect')
  .events.list({
    filter: { 'meta.status.$in': ['failed', 'cancelled'] },
  })

const records = response.data.data

By default the CLI prints a compact table with handle, status, bridge, effect, signal, replay, record, and linked — enough to identify a delivery, but meta.proofs and meta.output are not shown. Pass --verbose (alias -v) to dump the full raw response for every row, which is what you need to read the proof chain. The SDK list call returns the full records natively.

Pick a failing row, then read its proofs: each failed proof carries a reason (e.g. delivery.target-rejected) at the top of custom, with the specifics (httpStatus, body, …) under custom.detail. If the last proof is cancelled with custom.reason: "delivery.retry-cap-exhausted", the ledger has given up and no further attempts are scheduled automatically.

Retry

CLI:

minka effect events retry my-effect <delivery-handle>

SDK:

await sdk.effect
  .with('my-effect')
  .events.retry({ handle: '<delivery-handle>' })
  .hash()
  .sign([{ keyPair }])
  .send()

Confirm

Re-run the list call — the delivery should move from cancelled / failed to pending, then running, and finally delivered if the target now accepts. The proof chain grows by one pending → running → delivered cycle, preserving the full history of previous attempts.

The bridge-side flow is identical — swap effect for bridge (minka bridge events … or sdk.bridge.with(...)) in each command.

Verify the wire payload

meta.output is the same LedgerRecord object the webhook or bridge received — the same one on every retry of the same delivery. It's signed by the ledger and verifiable against the ledger's public key, so you can compare a received wire body against meta.output to confirm it wasn't altered in transit.

The shape of meta.output depends on what triggered the delivery:

  • Effect deliveries carry a LedgerRecord<LedgerEvent>LedgerEvent is the typed event payload the ledger emits when a signal fires (one event class per signal, all from @minka/types). The event's own evt_* handle lives at meta.output.data.handle and is identical across every delivery the same emit produced, so you can use it to correlate a webhook delivery with its bridge-side counterpart when one signal triggers multiple effects.
  • DTC bridge deliveries carry a LedgerRecord of the intent itself going through the two-phase commit. There's no LedgerEvent for these, so there's no evt_* correlation identifier — use the delivery's own data.handle, or filter by data.linked to list every delivery tied to one intent.

Proofs and history

Every transition the delivery goes through is signed by the ledger and appended to meta.proofs, in order. The resulting chain is an auditable timeline — it tells you exactly when each attempt started, what the target answered, how many times the ledger retried, and (if applicable) why it eventually gave up.

All proofs share the same envelope (method, public, digest, result, custom). The custom.status field distinguishes which transition the proof is marking. Where a reason is shown below, it's a LedgerErrorReason — the ledger's namespaced error enum, dotted strings of the form <namespace>.<code> (here, the delivery.* namespace).

custom.statusWhen it's appendedOther custom fields
pendingOn initial schedule, and on every retry schedule.
runningWhen the ledger starts the HTTP call for an attempt.
deliveredThe target answered 2XX. Terminal.detail.httpStatus (the 2XX code).
failedThe target answered non-2XX, errored, or timed out. Will be automatically retried.reason — one of three delivery.* values (see table below); plus detail.
cancelledLedger decided to stop retrying — appended directly after the final failed.reasondelivery.retry-cap-exhausted or delivery.permanent-failure. No detail.

failed proofs carry one of three reason values, each with its own detail shape:

reasonWhen it's useddetail shape
delivery.target-rejectedTarget responded with a non-2XX status.{ httpStatus, body? }body truncated to 500 chars.
delivery.target-unreachableNetwork failure — timeout, DNS, connection-refused, TLS error.{ message, code? }code is the Node error code.
delivery.unexpected-errorUnexpected ledger-side error while attempting the call.{ message, reason? }reason set when a structured ledger error was thrown.

A typical chain

A delivery that succeeded on its first attempt:

pending → running → delivered

A delivery that failed once, then succeeded on the retry:

pending → running → failed → pending → running → delivered

A delivery that exhausted the retry cap — the middle attempts are omitted for brevity:

pending → running → failed → pending → running → failed → … → pending → running → failed → cancelled

Note how the target's error lives on the last failed proof, and the trailing cancelled carries only the ledger's own reason for giving up. They're separate facts: "bridge returned 500" is a target-side fact, "we stopped retrying because the cap was hit" is a ledger-side decision.

Using the chain to investigate

  • How many times was it tried? Count running proofs — one per attempt.
  • What did the target say on each attempt? Read detail.httpStatus + detail.body on each failed / delivered proof, in order.
  • Why did the ledger stop? Look for a trailing cancelled proof — custom.reason tells you whether the target declared permanent failure (delivery.permanent-failure) or whether the retry budget ran out (delivery.retry-cap-exhausted).
  • When did each step happen? Every proof is signed at the moment of the transition, so the chain ordering is the timeline.
  • Was anything tampered with? Each proof is verifiable against the ledger's public key and digests the delivery's immutable hash — the chain can't be silently rewritten.

Planned changes

This surface will still evolve:

  • maxAge on retry will be replaced by a meta.moment range filter, so retry scope uses the same grammar as list.
  • data.handle on retry will accept richer operators (e.g. $in) so multiple specific deliveries can be retried in one call.
  • More filterable fields will become available on list.

On this page