Webhooks
Retries & Timeouts

Retries & Timeouts

Retry Behavior

Retry behavior depends on whether the webhook is synchronous or asynchronous — they use two different delivery paths.

WITHDRAW_REQUEST

WITHDRAW_REQUEST is delivered synchronously by default, but asynchronously (via the durable outbox, same as below) when your operator is on external-approval mode — in that mode the decision comes from your POST /withdraw/:id/decision callback, not from the webhook response. See Withdrawals.

Synchronous mode: delivered inline while AbstraPay waits for your approve/deny response. The inline client retries up to 2 times with a 5-second delay between attempts. Only network-level errors are retried:

  • Connection reset (ECONNRESET)
  • Timeout (ETIMEDOUT)
  • Connection refused (ECONNREFUSED)
  • Host unreachable (EHOSTUNREACH)
  • HTTP 5xx, 408, 429 responses

Application-level errors (non-zero error code in the response) are not retried.

Asynchronous — DEPOSIT, WITHDRAW_COMPLETE, and the headless terminal events

These events (including the headless-only WITHDRAW_FAILED and WITHDRAW_CANCELLED) are delivered through a durable outbox, not inline. If your endpoint is unreachable or returns an error, the outbox retries up to 5 attempts with exponential backoff (~20s, then 40s, 80s, … capped at 15 minutes between attempts). After the attempts are exhausted, the event is moved to a dead-letter queue for manual retry from the management panel (see Management Panel) — nothing is silently dropped.

⚠️

Make your handler idempotent. Because the outbox retries, the same DEPOSIT or WITHDRAW_COMPLETE event may be delivered to you more than once. Deduplicate deposits on idempotencyKey and withdrawal completions on withdrawRequestId (the withdrawal UUID — the same id sent on WITHDRAW_REQUEST; a separate sessionRef field carries the session correlation handle), and ignore duplicates, so you never credit or debit a user twice. Headless withdrawals: WITHDRAW_COMPLETE is the same payload as the widget's and deduplicates on withdrawRequestId too; the headless-only WITHDRAW_FAILED / WITHDRAW_CANCELLED deduplicate on eventId (one withdrawal can emit WITHDRAW_FAILED more than once — see Events). A redelivery is byte-identical.

🚫

Delivery-retry is not the same as retryable. The outbox retry above is a transport mechanism: it re-sends a webhook your endpoint failed to acknowledge. The retryable field on a headless WITHDRAW_FAILED is a different thing entirely — an accounting signal about the withdrawal: retryable:"true" means the failure is non-terminal, so you keep the user's reserve and wait for a later terminal event; retryable:"false" (or a WITHDRAW_CANCELLED) releases it. Acknowledge every delivery with { "error": 0 } regardless; drive your ledger from retryable, not from whether the webhook was retried. See Event Types → WITHDRAW_FAILED.


Timeouts

TimeoutValue
Connect10 seconds
Headers10 seconds
Body15 seconds

Ensure your endpoint responds within these limits.