Skip to content
Like what we’re building? Star on GitHub

Webhook receipts and launch retries ​

A claimed inbound delivery answers 202 with {"status":"accepted","delivery_id":"…"} before its bot launches. A fan-out also returns launches, with one outcome and delivery ID per claimed bot. The receipt promises a recorded claim, not a successful run. Read the eventual status, run_id, attempts, failed_at and next_attempt_at at GET /api/teams/{id}/webhooks/{webhook_id}/deliveries.

The shared path covers GitHub PR events, slash commands, review-thread replies and issues, and the GitLab, Forgejo and generic direct-launch handlers. A board coordinator's carded response and the push debounce's deferred response keep their own execution owners.

Connection and shutdown ​

The HTTP adapter registers the delivery worker with the server's existing tryGoSafe shutdown join. It returns the network response when the claim is acknowledged, including HTTP/2's end-of-stream. The worker uses launchBookingContext: context.WithTimeout(context.WithoutCancel(ctx), 30s). Identity and tracing values survive; request cancellation does not cancel the launch. Each claimed target and its final bookkeeping use this same bounded primitive. A fan-out confirms concurrency again before each execution.

Shutdown stops admitting new deliveries and stops the periodic sweep. A launch already claimed is not canceled by that signal. Shutdown joins it within the existing shared background budget (500ms by default, also bounded by the shutdown caller's deadline); an overrun is logged. The worker retains its own deadline. This is a bounded drain, not a guarantee against process termination. A process crash or a failed final store write can leave an accepted row: automatic retry of an uncertain launch would risk creating a second run, so this sweep does not reclaim such rows.

Automatic retry ​

Every 20 seconds the sweep reads at most 50 due failed deliveries. Memory and Mongo implement the same selection and ClaimFailedRetry compare-and-set: only the reader of the still-current failed attempt may claim it. An old sweep snapshot cannot consume the backoff of a newer attempt.

A claimed direct launch stores its resolved target before generating per-run publish grants. This internal target survives Mongo persistence and includes the bot, vars, repository/ref and trust/commit pin; it is excluded from delivery JSON responses. The raw webhook payload is not stored. A retry reads the current webhook config, checks its enabled state, tenant, provider, bot scope and trust, then passes ordinary tenant admission. A super-admin's retry does not bypass the webhook tenant's quota.

The first failed attempt waits 30 seconds. Later failures double that delay up to 10 minutes. The default ceiling is 8 total attempts, initial attempt included; ITERION_WEBHOOK_MAX_ATTEMPTS=<positive integer> changes that ceiling. A retry admission failure also consumes an attempt and schedules backoff. At the ceiling the delivery becomes launch_exhausted, with no next retry. A removed/disabled webhook, withdrawn bot scope or incompatible provenance also ends the chain with launch_exhausted and an explanatory error. The durable subject audit also ends an obsolete retry: a later PR/MR closure covers its comment and thread replies, and with overlap=supersede a later claim of the same bot/subject takes precedence. This check includes forge redeliveries and closes received while the failed launch was still in flight. It uses exact tenant/webhook/project scope and retains nanosecond receipt ordering across Mongo persistence.

The sweep owns direct inbound launches only. Push-debounce, gate relaunch and gate auto-fix retain their existing recovery policies and current-forge checks. Pre-claim audit failures and old rows without a saved target cannot be replayed through this endpoint; send a fresh authenticated event after fixing their cause.

Manual retry ​

http
POST /api/webhooks/deliveries/{id}/retry
Authorization: Bearer <super-admin access token>

No body. A successful claim answers 202 with the original delivery_id and status: accepted. It bypasses the scheduled delay, not the attempt ceiling. The claim races safely with the periodic sweep and forge redelivery.

HTTP statusMeaning
401 / 403Authentication / super-admin authority required
404delivery_not_found
409delivery_not_retryable; status names the delivery state
500Delivery store failure
503server_draining or webhook stores unavailable

A healthy launched delivery is refused with 409, as are accepted, launch_exhausted, target-less audit rows and failures carrying a run ID. The original idempotency key and delivery ID remain unchanged through retries. If a launcher reports a persisted run despite its error, that run ID is retained and no second launch is permitted. If the launcher reports that publication may have landed without naming a run, the status is launch_uncertain and no automatic or manual launch retry is permitted. Inspect the existing run/queue outcome rather than starting another run.

The executable witnesses and their mutations are in webhooks_durability_test.go; the paired store contract is in delivery_retry_test.go.