Browse documentation
Concept Webhook ingestion

Webhook deduplication, retries, and troubleshooting

Choose stable delivery IDs, interpret accepted duplicates, and retry only transient webhook-ingestion failures.

View as Markdown

Webhook senders must treat an unobserved 202 as ambiguous and retry safely. Hodoflow deduplicates a stable delivery identity per organization, so a retry can be acknowledged without starting the workflow twice.

Choose one stable delivery ID

The first non-empty header in this order becomes the delivery identity:

  1. Idempotency-Key
  2. X-GitHub-Delivery
  3. X-Shopify-Webhook-Id
  4. X-Twilio-Idempotency-Token

Prefer Idempotency-Key for a custom sender. Generate it once for a logical event, persist it with the outbound job, and reuse it across every retry of that event. Do not use X-Request-Id: it identifies one attempt and changes on retry.

Without one of these headers, Hodoflow hashes the trigger token and raw body in a two-hour time bucket. Byte-identical attempts in the bucket deduplicate, but a retry crossing the bucket boundary can run twice. The explicit header is the reliable choice. Delivery guard rows are retained for seven days by default.

Accepted and duplicate responses

The first successful delivery returns:

{"status":"accepted","workflow_run_id":"550e8400-e29b-41d4-a716-446655440000"}

A recognized retry returns the same 202 status but no new run ID:

{"status":"duplicate"}

duplicate means no second run was created. Keep the run ID from the first observed accepted response. If that response was lost, reconcile using your business identifier and workflow-run observability rather than sending a new delivery ID.

Retry matrix

Status Runtime body Action
202 accepted or duplicate Stop delivery retries
401 missing_credentials Exchange once if expired, then retry once with the same delivery ID
401 Unauthorized Fix selected-account, organization, or connector authentication
403 Insufficient scope Grant webhooks; do not retry unchanged
404 Trigger not found Confirm token and active workflow; do not retry unchanged
413 Request body too large Reduce the body; a byte-identical retry cannot succeed
422 Processing failed or Unsupported trigger authentication Fix payload or trigger configuration
429 Rate limit exceeded Wait the response's Retry-After, then retry with the same ID
503 Processing temporarily unavailable Wait Retry-After, back off, and retry with the same ID

Use exponential backoff with jitter for 503, cap the attempt count and total elapsed time, and preserve the delivery ID. For 429, the server's Retry-After is authoritative. Never turn a permanent 401, 403, 404, 413, or 422 into an infinite retry loop.

Limits and checkpoints

The default maximum body is 1,048,576 bytes and the default rate is 100 requests per trigger in a 60-second fixed window. Both are deployment-configurable. Check status, Retry-After, and the response body on every attempt.

An accepted response means the run was committed and enqueued, not completed. Read /api/json/v1/workflow_runs/:id using api_read, then follow the run state as described in Follow a workflow run.

Diagnose without exposing credentials

Record a local sender attempt ID, stable delivery ID, response status, x-request-id when present, and timestamp. Redact URL path tokens and every credential. When the trigger returns 404, confirm the workflow remains active; deactivation deliberately makes a real token indistinguishable from an unknown one.

The legacy organization-qualified path still routes but is deprecated:

POST /api/v1/webhooks/:org_domain/:token

Use the token-only path. It avoids coupling the sender to an organization-domain rename.

Next steps

Related documentation