Docs

Webhook receiver, execution runner, and stats API — the real surface.

Receive failed-payment webhooks

Register https://<your-host>/webhook in the Stripe dashboard (events: invoice.payment_failed, invoice.paid). Requests are HMAC-verified with STRIPE_WEBHOOK_SECRET using Stripe's real signature scheme. The receiver is fail-closed: with no secret configured it refuses requests (503) rather than skipping verification. Hosted status: the API is built and tested; the hosted deploy is pending hosting capacity.

curl -X POST localhost:5000/webhook \
  -H 'Content-Type: application/json' \
  -H "Stripe-Signature: t=1700000000,v1=<hmac>" \
  -d '{"type":"invoice.payment_failed","data":{"object":{
    "id":"in_123","customer":"cus_1","customer_email":"don+dunnguard@lifeofmorr.com",
    "amount_due":4900,"currency":"usd",
    "last_payment_error":{"decline_code":"insufficient_funds"}}}}'
# {"failure_id":7,"decline_code":"insufficient_funds",
#  "retry_plan_days":[1,3,5,9],"retryable":true,
#  "nudge_subject":"Payment didn't go through — we'll retry automatically"}

Retry plans

import scheduler
scheduler.plan_for("insufficient_funds")  # [1, 3, 5, 9]
scheduler.plan_for("expired_card")        # [] — don't retry, nudge now
scheduler.is_retryable("fraudulent")      # False
scheduler.nudge_for("do_not_honor")       # {"subject": ..., "body": ...}

Recovery stats

curl -H "Authorization: Bearer <DUNN_API_KEY>" \
  localhost:5000/stats
# {"total":12,"recovered":5,"lost":2,"in_retry":5,
#  "recovered_cents":24500,"lost_cents":9800}

GET /stats requires the API key — and refuses (503) when no key is configured, rather than going public. Stats are fed by the execution runner's recorded outcomes.

Execution runner

python3 runner.py --once            # one pass (cron / timer)
python3 runner.py --interval 3600   # loop in-process

The runner executes each failure's plan on its plan days — idempotency keys on every Stripe attempt, the 3-touch nudge sequence via the sender interface, a stand-down whenever Stripe Smart Retries already has a retry scheduled, and the card networks' attempt ceilings enforced before every attempt. It refuses to start until STRIPE_WEBHOOK_SECRET and STRIPE_API_KEY are set.

Health

curl localhost:5000/health
# {"ok":true,"service":"dunnguard"}