Retry & Delivery Policy
Delivery flow
- When an event occurs, TokenCashFlow creates a
webhook_delivery_jobrecord and queues it - The webhook worker picks up the job and sends an HTTP POST to your configured URL
- Timeout: 10 seconds per attempt
- Success: HTTP 2xx response → job marked
success; no further attempts - Failure: Non-2xx response, connection error, or timeout → job marked
failed; retry scheduled
Retry schedule
Retries use an increasing backoff schedule:
| Attempt | Wait before this attempt |
|---|---|
| 1 (initial) | — (immediate) |
| 2 | 30 seconds |
| 3 | 1 minute |
| 4 | 5 minutes |
| 5 | 10 minutes |
| 6 | 30 minutes |
| 7 | 1 hour |
| 8 | 2 hours |
| 9 | 4 hours |
| 10 | 8 hours |
| (beyond 10) | 24 hours each — only reachable if your job's max_attempts is raised above the default |
After the attempt count reaches the job's max_attempts (default 10), the job transitions to permanently_failed. No further delivery attempts are made automatically — but you can re-queue the job at any time (see below).
Total retry window: at the default max_attempts of 10, the 10 attempts span about 15.5 hours of scheduled waits (30 s + 1 m + 5 m + 10 m + 30 m + 1 h + 2 h + 4 h + 8 h = 55,790 seconds) from the first attempt to the last — budget your receiver's recovery window accordingly.
After permanent failure
Once a job reaches permanently_failed:
- The delivery log is retained and viewable in Dashboard → Webhooks → Delivery History
- You can retry it manually — click Retry in the portal or call
POST /v1/webhooks/deliveries/{job_id}/retry. The attempt counter resets and the delivery gets up tomax_attemptsfresh attempts. - You can still retrieve payment status via
GET /v1/payments/{payment_id}to reconcile your system
If your endpoint was down for an extended period, query the API to reconcile any events you may have missed.
If no active webhook configuration exists for your account (or the config was deactivated), pending jobs are marked
permanently_failedimmediately rather than retried.
What counts as success
Any HTTP response in the 2xx range (200–299) is treated as success. The body content is ignored.
Your endpoint should return a 2xx status quickly — do not perform long-running operations synchronously in the webhook handler. Acknowledge the webhook immediately, then process asynchronously.
Idempotency
Webhooks are delivered at-least-once. Duplicate deliveries are rare but possible (e.g. your endpoint returned 2xx but the network timed out before TokenCashFlow received the response).
Design your handler to be idempotent:
def handle_payment_completed(payment_id: str, net_amount_usd: float):
# Check if already processed
if Order.objects.filter(payment_id=payment_id, status="fulfilled").exists():
return # Already handled — safe to ignore
# Process for the first time
order = Order.objects.get(payment_id=payment_id)
order.status = "fulfilled"
order.net_amount = net_amount_usd
order.save()
send_fulfillment_email(order)Use data.payment_id + event as a natural deduplication key (or the X-TCF-Delivery header, which uniquely identifies a single delivery job).
Monitoring delivery health
- Dashboard: Client Portal → Webhooks → Delivery History shows per-event status and attempt counts
- API:
GET /v1/webhooks/deliveries?status=permanently_failedto programmatically check for failures - Alert threshold: If too many deliveries permanently fail, contact support to investigate your endpoint health

