BillingWebhookKit

Lemon Squeezy duplicate webhook retry

Lemon Squeezy webhook retry and idempotency guide

Duplicate webhook delivery is not a bug. Lemon Squeezy can retry after timeouts, 5xx responses, network failures, or uncertain delivery. Your handler must prove it can process a paid event once without sending duplicate downloads, license keys, emails, or access grants.

Generate idempotency key Read replay test guide Download free sample

Retry-safe launch gates

Authenticate first

Verify x-signature before trusting event fields

Only derive idempotency keys after raw-body signature verification. Bad signatures should return 400, not run fulfillment or create durable keys.

Store once

Persist the key before side effects

Write lemonsqueezy:order_created:order_id or an equivalent unique key before sending email, downloads, license keys, or account access.

Replay three times

Assert fulfillment runs once

Replay the same paid fixture repeatedly. The first request should fulfill; later duplicates should return success and skip side effects.

Return the right status code

Return 2xx for safe duplicates

If a paid order was already processed, return 2xx and skip duplicate fulfillment. Do not make Lemon Squeezy keep retrying an event that your system already handled.

Return 400 for unauthenticated input

Missing or invalid x-signature, wrong product, unsupported event type, or malformed payload should fail cleanly without writing fulfillment state.

Return 500 only for retryable fulfillment

If a verified paid event cannot be durably stored or delivered, return 500 only after your handler can retry without duplicate side effects.

Quarantine unknown events

Unknown events should produce a reviewable report, not a crash. Record the event name and safe metadata without exposing customer data or secrets.

Minimal process-once shape

Keep the idempotency guard close to the first durable write. The exact database API can vary, but the contract should stay stable.

export async function handleLemonPaidOrder(event) {
  const orderId = event.data?.id;
  const eventName = event.meta?.event_name;
  const key = `lemonsqueezy:${eventName}:${orderId}`;

  const inserted = await insertIdempotencyKey(key);
  if (!inserted) {
    return { ok: true, duplicate: true };
  }

  await deliverPrivateDownload(event);
  await grantBuyerAccess(event);
  await sendDeliveryEmail(event);

  return { ok: true, duplicate: false };
}

Evidence to collect before checkout goes public

  1. A signed order_created fixture that returns 2xx.
  2. A replay of the same fixture that returns 2xx and records duplicate: true.
  3. A side-effect counter showing one download email, one license, and one access grant.
  4. A malformed signature test proving unauthenticated payloads return 400.
  5. A retryable storage or delivery failure test proving 500 does not duplicate fulfillment on retry.
  6. A secret-free release report that excludes API keys, signing secrets, customer data, and private checkout links.

Convert retry safety into launch confidence

Safety boundary: use fake payloads and redacted logs in public reports. Never paste Lemon Squeezy API keys, webhook signing secrets, private checkout links, customer data, database URLs, or full live order payloads into shared debugging artifacts.