Webhook deliveries
Webhook deliveries
The monitor combines real, retained webhook delivery records with outbox events that have no retained delivery. An event-only row is not an invented HTTP attempt. One event targeting multiple endpoints can have multiple delivery rows; an event with retained deliveries does not also receive a synthetic event-only row.
Paths and permissions
GET /api/operator/webhooks/deliveries
GET /api/operator/webhooks/deliveries/export
POST /api/operator/webhooks/deliveries/{id}/replay
POST /api/operator/webhooks/deliveries/resend
- List and export require
webhooks:read. product=polyflightandproduct=alladditionally requirepolyflight:read, unless the caller has*.- Replay requires
webhooks:writeand a real delivery ID with an active endpoint. - Queued single/bulk resend requires
webhooks:write; historical event targets resolve currently active, subscribed endpoints. - All results are scoped to the authenticated operator, including endpoint joins. List, export, and replay use a fresh uncached database connection; list/export return
Cache-Control: private, no-store. Database unavailability fails closed with503 WEBHOOK_MONITOR_FRESH_DATABASE_UNAVAILABLE.
Shared list and export filters
Every filter is optional; all supplied filters combine with AND. List and export use the same validation, product permissions, filters, and ordering. Unknown parameters, repeated parameters, malformed controls, unsupported enums/event types, and reversed date ranges return 400; invalid controls are not silently clamped.
| Parameter | Type | Values/default |
|---|---|---|
product | enum | opinion (default), polyflight, all; classified by the outbox event type. |
status | enum | Any status in the table below. |
event_type | string | Exact supported webhook event type. |
created_from | ISO datetime | Inclusive lower bound with explicit timezone, for example 2026-09-01T00:00:00.000Z. |
created_to | ISO datetime | Inclusive upper bound, for example 2026-09-01T23:59:59.999Z. |
date_field | enum | event (default) or delivery. |
record_kind | enum | all (default), delivery, event. |
endpoint_id | positive integer | Exact currently retained endpoint ID; excludes event-only rows. |
delivery_id | positive integer | Exact real delivery ID; excludes event-only rows. |
event_id | string | Exact public event ID, maximum 128 characters. |
order_id | positive integer | Exact Opinion order ID. |
trade_id | positive integer | Exact Opinion executed trade ID. |
market_id | positive integer | Exact Opinion market ID for orders and redemptions; excludes Polyflight rows. |
external_user_id | string | Exact, case-sensitive user ID from the operator's own system, maximum 255 characters; covers Opinion orders/redemptions and supported Polyflight ticket, purchase-request, and payment-obligation records. |
purchase_request_id | UUID | Exact Polyflight purchase request correlation. |
ticket_id | UUID | Exact Polyflight ticket correlation. |
response_class | enum | 2xx, 3xx, 4xx, 5xx, none; none includes rows with no retained HTTP status. |
sort | enum | event_at_desc (default), event_at_asc, next_retry_at_asc; stable ID tie-breakers. |
page | positive integer | Default 1, maximum 10000. Export validates but ignores pagination. |
page_size | positive integer | Default 50, maximum 100. Export validates but ignores pagination. |
Numeric identifiers are decimal integers from 1 through 2147483647. UUID filters use the same version/variant rules as the other Operator API UUID parameters. Empty optional filters are omitted. external_user_id is matched verbatim, including surrounding whitespace and leading zeros; it is not a partial search or an internal Polynion/operator-user record ID.
Date basis and provenance
With date_field=event, bounds apply to the original outbox event's occurrence/creation time, even if its delivery record was created later. With date_field=delivery, bounds apply to delivery creation time and event-only rows are excluded, including when no date bounds are supplied. Omitting both bounds means all retained history. The dashboard sends complete UTC-day boundaries for day selections.
created_at on each result is the retained delivery's creation time, or the original event creation time for event-only rows. event.created_at always preserves the event time. disposition_recorded_at is when the dispatcher observation or historical baseline was recorded, not a delivery time or a reconstructed historical endpoint-change timestamp.
Monitor statuses
| Status | Meaning |
|---|---|
pending | A real delivery is pending, or an event without retained deliveries is still pending/dispatching. The latter is not a terminal claim that it was skipped. |
sent | The retained delivery received a 2xx acknowledgement. This alone does not prove a separate wallet effect. |
failed | Retained delivery failure, or failed outbox state when no delivery exists; inspect the relevant retry state. |
dead_letter | Terminal delivery/outbox failure. Resend targets the original active endpoint for a retained delivery, or current active subscribers when no delivery is retained. |
no_endpoint | The dispatcher observed zero configured endpoints and scheduled no callback. No automatic retry; explicit historical resend is available. |
endpoint_disabled | The dispatcher observed configured endpoints but zero active endpoints and scheduled no callback. No automatic retry; explicit historical resend is available. |
not_subscribed | Active endpoints existed but none subscribed to this event type, so no callback was scheduled. No automatic retry; explicit historical resend is available. |
history_unavailable | A terminal dispatched event has no retained delivery history. Whether a callback was sent or received is unknown. No automatic retry; explicit historical resend may duplicate earlier receipt. |
Historical backfill labels terminal dispatched events lacking retained delivery history as history_unavailable; it does not infer that no endpoint existed. Endpoint deletion removes delivery rows, so missing rows are not proof of non-delivery. Original event timestamps, business data, and financial outcomes are not rewritten. The same honest fallback applies when delivery history disappears after baseline capture. Current endpoint settings and setup history do not recreate missing attempts or establish a past receiver response.
List response
{
product_scope: 'opinion' | 'polyflight' | 'all'
includes_multiple_products: boolean
date_field: 'event' | 'delivery'
items: Array<{
id: number | null
row_key: string // delivery:<id> or event:<outbox row id>
record_kind: 'delivery' | 'event'
market_id: number | null // Opinion market; null for Polyflight or unknown
external_user_id: string | null // operator's external user ID; null if unknown
status: 'pending' | 'sent' | 'failed' | 'dead_letter'
| 'no_endpoint' | 'endpoint_disabled' | 'not_subscribed' | 'history_unavailable'
attempt_count: number // retained delivery attempt counter; 0 for event-only
last_status_code: number | null
last_response_body_sample: string | null
next_retry_at: string | null
delivered_at: string | null
created_at: string
updated_at: string
endpoint: { id: number; url: string; status: string } | null
replayable: boolean
disposition_recorded_at: string | null
reason: string | null
event: {
id: number
event_id: string
event_type: string
product: 'opinion' | 'polyflight'
operator_id: number
aggregate_type: string
aggregate_id: string
payload_json: Record<string, unknown>
status: string // actual outbox state, distinct from monitor status
attempt_count: number // outbox processing counter, not retained HTTP attempts
next_retry_at: string | null
created_at: string
}
}>
pagination: { page: number; page_size: number; total: number; total_pages: number }
summary: Record<
'pending' | 'sent' | 'failed' | 'dead_letter' | 'no_endpoint'
| 'endpoint_disabled' | 'not_subscribed' | 'history_unavailable', number
>
}
summary and pagination.total describe all matching rows before pagination, including the applied status filter, using the same consistent query snapshot. Event-only rows have null delivery ID, endpoint, HTTP status, response sample and delivered time, with zero retained delivery attempts. Terminal skip/gap rows have null retry times and replayable=false. Use row_key as the stable UI key, never a made-up numeric delivery ID.
The top-level market_id and external_user_id are the identities shown in the dashboard's Market ID and User ID columns and exported as separate CSV columns. They resolve from operator-scoped durable business/user records, including Opinion redemptions, independently of reconstructed event.payload_json. market_id never substitutes a Polyflight flight ID. Unknown identities are null, not guesses from aggregate IDs, internal user IDs, or callback data. External user IDs remain strings, including 0 and leading-zero values. Identity resolution uses bounded batched lookups in the same list/export query snapshot, not one query per row.
event.payload_json is reconstructed callback data, not byte-exact signed request bytes. Opinion order events reuse the same durable order/user/operator/trade reconstruction and fee calculation as callback construction. Polyflight events normalize stored immutable event snapshot keys; they do not revalue amounts with current FX or terms. Where original reconstruction falls back to stored event data, missing amounts remain unknown, not zero. Pending payout/refund obligations (owed or manual_review) are not confirmed wallet credits. A Polyflight purchase fee is included in stake debit; a full refund/compensation restores the stake.
The default product scope remains Opinion-only. Callback Settings uses mixed-product recent rows only when Polyflight read access is available. The full monitor restores the applied filters from the URL and exports those filters, not unsubmitted draft values.
CSV export
GET /api/operator/webhooks/deliveries/export exports all rows matching the same applied filters and sort, independent of list pagination. The maximum is 5000 rows. Above that maximum the server returns 422 EXPORT_TOO_LARGE with data.code, data.limit, and data.total, plus guidance to narrow the filters. It does not return a truncated successful download.
The attachment is named webhook-deliveries.csv, uses text/csv; charset=utf-8, Cache-Control: private, no-store and X-Content-Type-Options: nosniff, and includes a header even when no rows match. Columns:
row_key,record_kind,event_id,event_type,product,operator_id,aggregate_type,aggregate_id,market_id,external_user_id,event_created_at,delivery_id,delivery_created_at,status,reason,disposition_recorded_at,endpoint_id,endpoint_url,attempt_count,http_status,delivered_at,next_retry_at,callback_data_reconstructed
market_id and external_user_id contain the same top-level identities returned by list for the matching records. Null values export as empty cells; Polyflight has no applicable Market ID. External IDs use the same CSV escaping and spreadsheet-formula protection as every other cell.
callback_data_reconstructed is JSON containing the reconstructed callback data, including available financial amounts and business identifiers, not the originally transmitted signed envelope. Reconstruction uses sequential bounded batches of joined order/user/operator/trade lookups, not thousands of individual lookups or unbounded concurrency. A reconstruction error fails the export rather than silently dropping data.
CSV uses the shared canonical escaping for quotes, comma/CR/LF cells, and spreadsheet formula prefixes. It contains no signing secret, raw signed request, or receiver response-body sample. Treat exports as sensitive operator data: endpoint URLs, customer identifiers and financial values may be present. Store and share them under your organization's access and retention rules; CSV escaping does not make the contents public-safe.
Replay rules
id is a required positive, real delivery ID path parameter. Replay has no query parameters or request body. The endpoint must still exist, be active, and belong to the authenticated operator. The API also scopes the outbox event to that operator.
Replay dispatches the same delivery object again and does not create a new business event. This retained-delivery route does not accept event-only rows; use the queued resend route below for historical backfill. Enabling/creating an endpoint does not automatically send historical events. Receivers must durably dedupe by the original event_id, especially when replaying a delivery that already returned any 2xx (not only 200). Replay never authorizes another financial effect.
“Original endpoint” means the same endpoint record, not a frozen historical URL or secret. Replay/resend uses that record's current URL, signing secret, timeout and attempt policy, even if its subscription list changed. Review saved setup history before replaying after a configuration change. Callback data is reconstructed by the normal sender, so logical event identity is preserved without promising byte-identical JSON, timestamps, or signatures.
An event already marked dispatching cannot be claimed concurrently. Replay returns 409 WEBHOOK_DELIVERY_NOT_REPLAYABLE if no eligible attempt starts (for example, an in-flight dispatch or an inactive endpoint), rather than returning a successful no-op. replayable is false while the event is dispatching or carries a terminal dispatch disposition. A timeout or interruption is not permission to resend automatically; inspect the retained state before taking another action.
The retained-delivery replay route attempts dispatch during the request; it is not the asynchronous queue API. Its response is dispatch evidence, not a wallet result. Use the queued route when scheduling historical or bulk work.
Queued single and bulk resend
POST /api/operator/webhooks/deliveries/resend durably queues 1–100 selected records before responding. The dashboard uses this route for both single and bulk resend; it does not wait for receiver HTTP requests.
{
"items": [
{ "delivery_id": 123 },
{ "event_id": 456 }
]
}
Each item has exactly one key. delivery_id identifies a retained delivery; event_id is the numeric outbox row ID (event.id in list results), not the public string event.event_id. IDs must be integers from 1 through 2147483647. Unknown keys, malformed targets, empty/oversized lists and duplicate targets are rejected.
Retained deliveries target their original active endpoint only. Historical targets must still be terminal dispatched or dead_letter events with no retained deliveries, including skipped dispatches and history_unavailable. They materialize pending deliveries for all currently active, subscribed endpoints of the authenticated operator; no eligible endpoint means rejection. Missing history is not proof of non-delivery, so backfilling it may duplicate a prior callback.
The entire batch is atomic and operator-scoped. Malformed requests return 400, missing/foreign records return 404, and ineligible targets, in-flight dispatches or unavailable endpoints return 409. A rejection queues none of the batch. Concurrent event claims serialize scheduling and dispatch. No new business event or financial mutation is performed; original public event ID, event data and occurrence time are retained.
Success returns { ok: true, queued_count: number, results: Array<{ delivery_id: number } | { event_id: number }> }. The count is selected records, not endpoint requests. The existing dispatcher processes queued work asynchronously using the same callback payload construction, signature headers, and real HTTP POST as normal delivery; backfill is not a simulated request. Refresh the monitor to inspect actual attempts and acknowledgements. Queuing preserves retained HTTP evidence and attempt counts. Pending/due failed deliveries are eligible for dispatch; successful or unselected terminal siblings are not resent. Retries do not discover newly configured endpoints for an already materialized event.
Delivery order is not guaranteed. A queue response does not prove receipt. If the request times out, inspect current retained state before resubmitting; receivers must deduplicate by the original public event_id.
Already pending deliveries cannot be queued again; wait for their existing attempt instead. A manual resend retains its lifetime attempt counter, not a fresh retry budget. Even an exhausted delivery gets the explicitly requested attempt; if it fails after reaching the endpoint's maximum, it returns to dead letter without additional automatic retries.
Useful conflict codes are WEBHOOK_EVENT_BUSY, WEBHOOK_ENDPOINT_DISABLED, WEBHOOK_DELIVERY_ALREADY_QUEUED, WEBHOOK_EVENT_NOT_BACKFILLABLE, and WEBHOOK_NO_ELIGIBLE_ENDPOINT. The error's data.target identifies the rejected selected item. Fix that cause and refresh the whole selection before retrying; a batch conflict does not partially queue the other items.
