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:
Idempotency-KeyX-GitHub-DeliveryX-Shopify-Webhook-IdX-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
- Verify the webhook operation reference.
- Add HMAC verification.
- Rehearse an unobserved-
202retry in a non-production workspace and confirm the second response isduplicate.