Delivery & Retries
How GivePay delivers webhooks and what to expect on failure
Delivery pipeline
When a payment, refund, or subscription event occurs:
- GivePay creates a webhook event record (with a stable
idand an idempotency key). - It finds every active endpoint in the matching environment whose
enabled_eventlist includes the event type (or"*"). - A delivery attempt is queued for each matching endpoint.
- Workers pull from the queue, sign the payload with that endpoint's secret, and
POSTit to your URL. - Failed attempts are re-queued with exponential backoff until a 2xx is received or the attempt cap is hit.
Retry policy
| Setting | Value |
|---|---|
| Max attempts | 8 per delivery (initial attempt + 7 retries) |
| Request timeout | ~30 seconds per attempt |
| Success | HTTP 2xx (200–299) |
| Retry triggers | HTTP 4xx, 5xx, network errors, TLS errors, timeout |
Retry schedule
When an attempt fails, the next retry is queued with a fixed delay. The delays escalate as the delivery accumulates failures:
| Attempt | Delay since previous failure | Cumulative time since first failure |
|---|---|---|
| 1 (initial) | — | 0 |
| 2 | 1 minute | 1 min |
| 3 | 5 minutes | 6 min |
| 4 | 30 minutes | 36 min |
| 5 | 2 hours | ≈ 2 h 36 m |
| 6 | 8 hours | ≈ 10 h 36 m |
| 7 | 24 hours | ≈ 1 d 10 h |
| 8 | 48 hours | ≈ 3 d 10 h |
After the 8th attempt fails, the delivery is marked EXHAUSTED and no further attempts are made. Your receiver therefore has ~3.4 days from the first failure to come back online before a delivery is permanently dropped from the retry queue. The dashboard surfaces exhausted deliveries so you can replay them once the issue is fixed.
Status values
| Status | Meaning |
|---|---|
PENDING | Queued but not yet attempted, or scheduled for retry |
SUCCEEDED | Receiver returned a 2xx |
FAILED | Last attempt failed; will retry until EXHAUSTED |
EXHAUSTED | All retry attempts used up |
Best practices
Always acknowledge fast
Respond with a 2xx as soon as you've durably captured the event (e.g. written it to a queue or database). Don't do slow work inline:
- Sending email or SMS
- Hitting third-party APIs
- Generating PDFs
A receiver that takes longer than ~30 seconds will be retried, leading to duplicates.
Deduplicate using event id
Webhooks are at-least-once. Persist a record of every event.id you've processed and skip duplicates:
async function handle(event) {
const inserted = await db.processedEvents.insertIfMissing(event.id);
if (!inserted) return; // already processed
await enqueueWork(event);
}Verify before parsing
Run signature verification on the raw bytes of the request body, before any JSON parsing. See Signatures.
Pin to event types you care about
Subscribing with ["*"] is convenient but means you'll receive event types added in future releases. Pin to specific types in production so a new event type doesn't surprise your code.
Use SANDBOX for testing
Create a separate environment: "SANDBOX" endpoint in the dashboard and exercise it with the dashboard's "Send test event" action and real sandbox checkouts. Sandbox events never touch a production receiver.
Monitor EXHAUSTED deliveries
The dashboard surfaces exhausted deliveries — hook that into your on-call alerting. An exhausted delivery means your receiver was unavailable for the full retry window.
Observability
For every delivery attempt, GivePay records and surfaces in the dashboard:
- The full HTTP request (method, URL, headers, body)
- The receiver's HTTP status code
- The receiver's response body, truncated to 1 KB
- Any transport-level error message
attempt_count,next_retry_at,last_attempt_at,completed_at
Source of truth
Webhooks are a notification mechanism, not the system of record. If your receiver has been down for an extended period or you miss events, reconcile with the REST API:
GET /payment-session/:slug— current state of a paymentGET /subscription/:slug— current state of a subscription
These return the same canonical objects emitted in webhook payloads.