Webhook Event Log and Replay
The gateway persists outbound webhook events and their delivery attempts so operators and integrators can inspect failures, replay events, and manage retention without digging directly through service logs.
This page describes the persisted webhook-event surface, the difference between event replay and DLQ retry, and the SDK helpers exposed from the normal admin clients.
Refund and void durability
Processing persists the canonical payment.refunded or payment.voided event
inside the same database transaction as the final financial projection. An
event-persistence failure aborts that local finalization and retains the
original operation for reconciliation; it must not cause a second processor
refund or void. Unresolved outcomes do not create success events.
The event and a permanent, Gateway-owned per-operation notification marker are committed together. Once marked, an operation never creates another canonical event, even after the original event row expires. The marker does not alter financial evidence or get copied from a parent payment to a new refund.
Unmarked legacy refunds and voids never automatically create a new event.
Recovery can verify an existing canonical event's ownership and retain the
permanent marker without emitting it again. If no event exists, every replay
logs approval required; historical gap repair requires an explicitly approved,
ownership- and evidence-bounded backfill, regardless of age.
There is no timestamp-window heuristic. A newly admitted retry can cache a row's
recent bookkeeping updated_at in a response, and late WAL closure is not a
financial approval time. Neither can turn a legacy operation into a new event.
Existing financial responses and reversal timing remain intact, including legacy
voids with unknown compensation evidence. Event replay is not a substitute for
creating a missing canonical event, and a VOIDED status alone does not establish
full reversal or compensation evidence.
Return-converted (A0014) refund replays recover the parent void, not a refund
success. A retained canonical void event may reference the parent or its verified
return-converted child. Recovery preserves the original requested-operation
outcome and never invents an amount or issues a provider inquiry.
Persisted fanout recovery delivers committed event rows independently of the request; it cannot discover a historical event that was never inserted.
Replay vs. DLQ retry
These two actions are related but not interchangeable.
Replay an event
Use event replay when you want to enqueue delivery again from the persisted event record.
- Endpoint:
POST /api/v1/webhook-events/{eventId}/replay - Typical use: replay the event to every active subscription, or to a filtered subset of subscriptions
- Result: new delivery attempt rows are queued for that event
Retry a DLQ delivery
Use DLQ retry when a specific delivery row already failed all automatic
attempts and was marked DLQ_PENDING or DLQ_EXHAUSTED.
- Endpoint:
POST /api/v1/webhook-deliveries/{deliveryId}/retry - Typical use: the merchant fixed their endpoint and you want to re-drive the exact failed delivery row
- Result: the gateway resets that delivery row back into the delivery worker
Rule of thumb:
- if you are operating from an event-centric view, use replay
- if you are operating from a delivery-row / DLQ view, use retry
SDK helpers
The persisted-event surface is exposed from the normal admin clients in both SDKs.
TypeScript
import {
GatewayAdminClient,
getWebhookEventDetails,
replayFailedWebhookEventDeliveries,
replayWebhookEventDeliveries,
} from '@peakgateway/sdk'
const client = new GatewayAdminClient({
environment: 'staging',
clientId: process.env.GATEWAY_CLIENT_ID,
clientSecret: process.env.GATEWAY_CLIENT_SECRET,
})
const details = await getWebhookEventDetails(client.webhookEvents, 'evt_123')
// Replay only failed / DLQ deliveries for the event.
await replayFailedWebhookEventDeliveries(client.webhookEvents, details.event.id)
// Or replay a specific subset of prior delivery rows.
await replayWebhookEventDeliveries(client.webhookEvents, details.event.id, {
deliveryIds: details.deliveries.filter((delivery) => delivery.status === 'FAILED').map((delivery) => delivery.id),
})
// Retry one DLQ delivery row directly.
await client.webhookEvents.retryDeliveryFromDlq('del_456')
Kotlin
import com.myriad.gateway.GatewayAdminClient
import com.myriad.gateway.GatewayConfig
import com.myriad.gateway.webhookevents.findRetentionConfigForOrganization
import com.myriad.gateway.webhookevents.getDetails
import com.myriad.gateway.webhookevents.replayDeliveries
import com.myriad.gateway.webhookevents.replayFailedDeliveries
suspend fun operateWebhooks() {
val client = GatewayAdminClient(GatewayConfig("client-id", "client-secret"))
val details = client.webhookEvents.getDetails("evt_123")
client.webhookEvents.replayFailedDeliveries(details.event.id)
client.webhookEvents.replayDeliveries(
eventId = details.event.id,
deliveryIds =
details.deliveries
.filter { it.status == "FAILED" }
.map { it.id }
.toSet(),
)
client.webhookEvents.retryDeliveryFromDlq("del_456")
val retention = client.webhookEvents.findRetentionConfigForOrganization("org_123")
client.close()
}
Delivery diagnostics
Persisted delivery rows can include:
- delivery status
- HTTP status from the destination endpoint
- attempt count
- last error message
- allowlisted response headers (
lastResponseHeaders) - replay lineage (
replayOfDeliveryId)
That data is the first place to look before jumping to raw service logs.
Correlation IDs inside received webhook handlers
If your receiver wants a durable way to map a received webhook back to gateway traces or support investigations, use the verifier helpers that preserve the delivery correlation id.
- TypeScript:
WebhookVerifier.verifyDelivery(...) - Kotlin:
WebhookVerifier.verifyDelivery(...)
Those helpers return the decoded event plus a correlationId extracted from
traceparent, X-Cloud-Trace-Context, or X-Request-Id headers when present.
Boundaries
Use only the authorized organization's event surface. Event delivery records are not a security audit trail. Receiver processing must tolerate duplicate delivery; replay does not repeat the original payment. An application delivery DLQ is not a PubSub queue and retrying a delivery does not drain infrastructure backlog.
For merchant-status store.authorized and store.revoked events, lifecycleVersion
is a monotonically increasing integer scoped to organizationId and locationId.
Both billing and manual status changes use the same counter. Persist the highest
version processed for that location atomically with the receiver's state change;
ignore a lower or equal version, while retaining event-ID deduplication. Versions
may skip values (some status transitions do not emit an event). Versioned
merchant-status deliveries include organizationId; the SDK exposes it as
optional because older OAuth store notifications can omit it. Do not treat
an absent version or organization as newer than a versioned merchant-status event.
The gateway transaction atomically fences child creation and dispatch admission, and a superseding transition cancels locally queued deliveries. An HTTP request admitted before the transition can still arrive or finish after the new event; network timeouts do not prove the receiver did not apply it. Subscription fanout and receiver processing are not ordered. The receiver's version check is needed to prevent a late old revoke from undoing a newer authorization.
For the human workflow see Support event log. See also Gift-card events and Device events.