PayHook

Payment webhooks compared: Stripe, Paddle, Lemon Squeezy, Polar, Dodo

How Stripe, Paddle, Lemon Squeezy, Polar and Dodo deliver webhooks: the status that counts, the timeout, retries, the id to deduplicate, the signature window.

A webhook handler written for one payment provider can quietly fail with another. Lemon Squeezy and Paddle count only a 200 as delivered, while Stripe, Polar and Dodo Payments accept any 2xx. Paddle waits 5 seconds for your answer, Dodo 30. Lemon Squeezy gives up after four attempts, about two and a half minutes with the delays it gives as an example, while Stripe and Paddle keep trying for three days. Polar disables an endpoint after 10 failed deliveries in a row. And Lemon Squeezy sends no delivery id at all.

This page puts the five side by side, from each provider's documentation as of 9 October 2026.

The table

WhatStripePaddleLemon SqueezyPolarDodo Payments
Counts as deliveredAny 2xx200200Any 2xxAny 2xx
Time to answerNot documented5 secondsNot documented10 seconds; answer within 230 seconds
RetriesUp to 3 days, exponential backoff; in a sandbox, 3 over a few hours60 within 3 days; in a sandbox, 3 within 15 minutes3 more, for example after 5, 25 and 125 secondsUp to 10, exponential backoff8 attempts in all, over about 27.6 hours
Failing endpointNot documentedNot documentedNot documentedDisabled after 10 failed deliveries in a row, with an email to the organizationNot documented; no email alerts
Id to deduplicateThe event's id (evt_…) in the bodyevent_id (evt_…) in the bodyNoneThe webhook-id headerThe webhook-id header
Signature headerStripe-SignaturePaddle-SignatureX-Signaturewebhook-signature (Standard Webhooks)webhook-signature (Standard Webhooks)
Signature window5 minutes in the SDKs5 seconds in the Node, Python and PHP SDKsNone: the signature has no timestamp5 minutes, both ways5 minutes, both ways
Manual resendDashboard for 15 days, CLI for 30; same event id, new signatureAPI replay of a delivered or failed notification, for 90 days; same event_id, new notification_idResend in the dashboard; the body is serialized againRedeliver in the dashboardReplay one message from the logs, or failed messages in bulk, up to two weeks back

What counts as delivered

Two of the five count only a 200. Lemon Squeezy's documentation says to return a 200 and that anything else is retried, and Paddle's delivery guide says the same of any other status code. So a handler that answers 201, 202 or 204, which Stripe, Polar and Dodo accept, runs again for every Lemon Squeezy or Paddle event, and the delivery still ends up failed. Answer exactly 200 and every provider is satisfied.

Stripe and Polar also treat a redirect as a failure and do not follow it, so the endpoint URL must be the final one: a www redirect on your host is enough to fail every delivery.

How long you have to answer

Paddle waits 5 seconds, Polar 10 (and asks for an answer within 2), Dodo 30; Stripe and Lemon Squeezy do not give a number. Whatever the number, a timeout counts as a failure and brings a retry, even when your work finished. So answer first and do the work after, from a queue or a background job, as Stripe, Paddle, Polar and Dodo all recommend.

Paddle's five seconds come back in its signature check: its Node, Python and PHP SDKs reject a signature more than 5 seconds old, so a slow tunnel fails both. See Paddle's signature error.

How long they keep retrying

The schedules differ by orders of magnitude. Lemon Squeezy makes four attempts; with the delays its documentation gives as an example, that is about two and a half minutes, so an endpoint that is down for three minutes loses the event. Dodo retries for about 27.6 hours, Stripe and Paddle for three days in live mode. Polar retries up to 10 times, and after 10 failed deliveries in a row it disables the endpoint and emails the organization's members; it stays disabled until someone enables it again in the webhook settings.

Test environments retry less: Stripe retries three times over a few hours in a sandbox, Paddle three times within 15 minutes.

Deduplicate, and do not trust the order

Every provider may deliver the same event twice, so keep the id of each event you have handled and skip repeats. Stripe and Paddle put it in the body: Stripe's event id, and Paddle's event_id, not its notification_id, which differs between destinations and replays. Polar and Dodo send it as the webhook-id header, which the Standard Webhooks specification keeps the same on every retry. Lemon Squeezy sends none, and its body's meta.webhook_id changes on a Resend: build a key from the event name, the resource and its updated_at, as our Lemon Squeezy page shows.

No provider promises the order of events. Paddle says to compare occurred_at, Dodo to order by the payload's timestamp, and Dodo adds that a delivery carries the latest state at the time it is sent. Stripe warns not to order by created: it is in seconds, and several events share it.

Signatures and their windows

Each scheme signs the raw body, so a body parsed and serialized again fails everywhere. The windows differ: Stripe's SDKs allow 5 minutes, Paddle's 5 seconds, Polar's and Dodo's Standard Webhooks libraries 5 minutes in either direction, and Lemon Squeezy's signature has no timestamp, so a captured request stays valid forever. The details, error by error: Stripe, Paddle, Lemon Squeezy, Polar and Dodo Payments.

How PayHook reports it

PayHook, the webhook inspector we are building, checks each event with its provider's own signature scheme and time window. When it relays events to your app, it keeps your app's answer next to each event and warns when the provider would count that answer as a failure, such as a 204 sent to Lemon Squeezy. PayHook is in closed beta; the home page has the details.

Sources

Back to the blog