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 exponential backoff schedule. The default schedule (configurable by the platform operator):
| Attempt | Delay after previous failure |
|---|---|
| 1 (initial) | Immediate |
| 2 | 30 seconds |
| 3 | 60 seconds |
| 4 | 5 minutes |
| 5 | 10 minutes |
| 6 | 30 minutes |
| 7 | 1 hour |
| 8 | 2 hours |
| 9 | 4 hours |
| 10 | 8 hours |
After all 10 attempts are exhausted the job transitions to permanently_failed. No further delivery attempts are made automatically.
Total retry window: approximately 16 hours.
After permanent failure
Once a job reaches permanently_failed:
- The delivery log is retained and viewable in Dashboard → Webhooks → Delivery History
- The admin team receives an alert (configurable)
- You can still retrieve payment status via
GET /v1/payments/{payment_id}to sync your system
If your endpoint was down for an extended period, query the API to reconcile any events you may have missed.
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.
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

