What counts as a failed webhook delivery
There is no special handling for
4xx. A 410 Gone does not disable the endpoint; only removing it in the dashboard does. If you want to drop a message on purpose, return 200.
The webhook retry schedule on Flexprice Cloud
Flexprice Cloud delivers through Svix, which retries with increasing gaps:
An endpoint that keeps failing for several days is disabled automatically and shown as such in the dashboard, and you are notified. Re-enable it once the handler is fixed.
Retries on self-hosted native delivery
Without Svix enabled, a self-hosted instance retries with exponential backoff and gives up after a short window. The defaults are 3 retries, a 1 second initial interval, a 10 second cap, a 2.0 multiplier, and 2 minutes of total elapsed time. All five are configurable; see Configuration. Because the window is short, put native deliveries on a queue as soon as they arrive.Replaying a failed webhook delivery
Open the endpoint in the dashboard, go to Logs, and choose a failed attempt to resend it. This uses the original payload, so thesvix-id is the same and your dedupe check treats it as the same message.
Handling duplicate webhook deliveries
Because delivery is at-least-once, treat every message as possibly repeated:- Key on
svix-id. It is unique per message and constant across retries. Keep it for longer than the retry window: the schedule above runs for about 28 hours after the first attempt, so a one-day TTL expires before the last retries arrive. Three days is a safe minimum. A dashboard replay after the record expires is processed as new. - Or key on the object. For state changes,
event_typeplus the object’sidplus itsupdated_atidentifies the change. This also protects against two different messages that describe the same state. - Make the effect idempotent. “Set invoice
inv_123to paid” can run twice safely. “Add 100 credits” cannot; look up the wallet transaction ID from the payload before crediting.
Webhook delivery ordering
Messages are sent in the order the events occurred, but retries and parallel delivery mean they can arrive in any order.subscription.updated can land before the subscription.created it follows, and a retried payment.pending can land after payment.success.
Handlers that are safe under reordering share one habit: they treat the payload as a snapshot, not a diff.
- Compare timestamps. Each object carries
updated_at. Ignore a message whoseupdated_atis older than what you already stored for that object, and make that comparison part of the write so a concurrent worker cannot slip between the two. - Read the status from the payload, not the event name.
invoice.updatetells you an invoice changed;invoice.invoice_statusandinvoice.payment_statusin the body tell you what it is now. - Fetch when in doubt. If two messages disagree, call the API for the object’s current state.
GET /invoices/{id}is cheap and always right.
A webhook handler shape that survives retries
- The dedupe check is an insert. Calling
seen()and thenmarkSeen()as separate statements leaves a window where two workers handed the same delivery both pass the check and both run the side effects. Svix can redeliver while your first attempt is still queued, so that window is real. A unique constraint on the delivery ID closes it. - The timestamp comparison is part of the write. Loading the row, comparing it in application code, and saving is three statements. Two workers can both pass the comparison, and the one carrying the older snapshot can commit last. A conditional update decides it in one statement. Taking a row lock before the read works too, as long as you hold it until the write commits.
- Both run in one transaction. A crash cannot then apply one without the other, so you do not have to reason about whether saving or marking first is the safer order.

