Inspect event deliveries
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/eventsAlso 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:
| Path | Description |
|---|---|
data.handle | Delivery handle. Stable across retries. Use this to retry a specific delivery. |
data.bridge | Set when the delivery targets a bridge (both DTC messages and bridge-targeted effect events). |
data.effect | Set when the delivery carries an effect event (regardless of whether that effect targets a bridge or a webhook). |
data.record + data.linked | Record type and handle that triggered the delivery (e.g. intent + <handle>). |
meta.status | One of pending, delivered, failed, cancelled. See Statuses. |
meta.replay | Number of completed attempts so far. |
meta.output | The same signed object that was sent on the wire. Replayed unchanged on every retry. See Verify the wire payload. |
meta.proofs | Signed 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.
| Status | Meaning |
|---|---|
pending | An attempt is scheduled. The ledger will call the target soon, or is waiting between retries. |
running | The 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. |
delivered | The target acknowledged with a 2XX response. Terminal. |
failed | The 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. |
cancelled | The 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:
| Path | Purpose |
|---|---|
meta.status | Find failed/delivered/pending deliveries. |
data.record + data.linked | All deliveries tied to a specific record (e.g. one intent). |
meta.output.data.signal | Which 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/retryAlso 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, orpending. Retrying adelivereddelivery 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.
maxAgeis 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"]}' --verboseSDK:
const { response } = await sdk.effect
.with('my-effect')
.events.list({
filter: { 'meta.status.$in': ['failed', 'cancelled'] },
})
const records = response.data.dataBy 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>—LedgerEventis the typed event payload the ledger emits when a signal fires (one event class per signal, all from@minka/types). The event's ownevt_*handle lives atmeta.output.data.handleand 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
LedgerRecordof the intent itself going through the two-phase commit. There's noLedgerEventfor these, so there's noevt_*correlation identifier — use the delivery's owndata.handle, or filter bydata.linkedto 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.status | When it's appended | Other custom fields |
|---|---|---|
pending | On initial schedule, and on every retry schedule. | — |
running | When the ledger starts the HTTP call for an attempt. | — |
delivered | The target answered 2XX. Terminal. | detail.httpStatus (the 2XX code). |
failed | The target answered non-2XX, errored, or timed out. Will be automatically retried. | reason — one of three delivery.* values (see table below); plus detail. |
cancelled | Ledger decided to stop retrying — appended directly after the final failed. | reason — delivery.retry-cap-exhausted or delivery.permanent-failure. No detail. |
failed proofs carry one of three reason values, each with its own detail shape:
reason | When it's used | detail shape |
|---|---|---|
delivery.target-rejected | Target responded with a non-2XX status. | { httpStatus, body? } — body truncated to 500 chars. |
delivery.target-unreachable | Network failure — timeout, DNS, connection-refused, TLS error. | { message, code? } — code is the Node error code. |
delivery.unexpected-error | Unexpected 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 → deliveredA delivery that failed once, then succeeded on the retry:
pending → running → failed → pending → running → deliveredA delivery that exhausted the retry cap — the middle attempts are omitted for brevity:
pending → running → failed → pending → running → failed → … → pending → running → failed → cancelledNote 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
runningproofs — one per attempt. - What did the target say on each attempt? Read
detail.httpStatus+detail.bodyon eachfailed/deliveredproof, in order. - Why did the ledger stop? Look for a trailing
cancelledproof —custom.reasontells 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:
maxAgeon retry will be replaced by ameta.momentrange filter, so retry scope uses the same grammar as list.data.handleon 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.