Home / Blog / License delivery
License delivery

Why does license issuing break when your payment webhook fires twice?

Payment providers retry webhooks, and they do it more often than you expect. If your license issuing is not idempotent, customers get two keys, or none.

Two identical brown cardboard parcels sitting side by side on the front step of a suburban house, afternoon sun, a person's hand reaching for one of them, no visible labels or text

Webhooks are at-least-once, and that is by design

Every payment platform documents that an event can be delivered more than once. A network timeout, a 500 from your endpoint, or a replay after an incident on their side will all trigger a retry, and some platforms also deliver events out of order. If your handler does 'on payment succeeded, create a license and email it', the second delivery creates a second license and sends a second email. The customer now holds two keys, your seat count is doubled, and a refund-triggered revocation only catches one of them. Related: Why Digital Products Need Licensing

The opposite failure is worse. Your handler creates the license, then fails while sending the email, returns an error, the provider retries, and you accumulate a stack of orphaned licenses that the customer never received. Both failures come from treating the webhook as a command to execute instead of a notification that something happened. Related: How do you activate a license when the customer's machine never touches the internet?

Keep reading: Why Digital Products Need Licensing, Generating License Keys That Work, Validating Keys Without Frustrating Users. See how KeyDispense helps you digital product license key issuing and validation api.

Idempotency keys are the fix, and the event ID is usually the key

Store the provider's event ID, or better the order ID, on the license record. Before creating anything, check whether a license already exists for that ID; if it does, return success and either do nothing or resend the same key. Duplicate deliveries become harmless. Enforce this with a unique constraint in the database rather than only an application-level check, because two deliveries can arrive within milliseconds and race past a read-then-write.

Then separate the steps: record the event, issue the license, deliver the license. Each should be independently retryable. If the email fails, the license already exists and can be resent later; the webhook should return success as soon as the event is durably recorded, with delivery queued. A fast acknowledgment also stops the provider from retrying simply because your handler was slow.

Handling out-of-order events and partial states

Subscription lifecycles generate many events: created, paid, updated, past due, canceled. If your endpoint was briefly down, a cancellation can arrive before the payment event it logically follows. Instead of reacting to each event in isolation, treat the event as a signal to fetch the current subscription state from the provider's API and set the license to match. State-based synchronization is idempotent by nature and order-independent. Related: What is the best way to structure trial keys so they convert to paid?

Give the license explicit states (pending, active, past due, suspended, canceled) and write the transition rules down before coding them. The webhook handler's only job becomes moving the license into the state the payment record says it should be in. This also makes it straightforward to run a nightly reconciliation that catches events you never received at all.

Testing and monitoring the pipeline before real money flows

Replay the same test event five times and confirm that exactly one license exists. Deliver events out of order. Kill the handler halfway through and replay. Most providers let you resend events from their dashboard, and that button is your best testing tool. Add an invariant check that fails loudly if the number of licenses for a single order ever exceeds one.

In production, alert on webhook failure rate and on licenses created without a matching payment record. Keep the event log for a long time. When a customer says they paid but got nothing, the fastest resolution is searching your stored events by their email and re-running delivery from there, with no need to involve the payment provider's support. Related: Protecting Your Product Without Punishing Customers

Key takeaways
  • Payment webhooks are delivered at least once and sometimes out of order; treat them as notifications, not commands.
  • Store the event or order ID with the license and enforce uniqueness in the database, not only in code.
  • Split recording, issuing, and delivering into independently retryable steps, and acknowledge quickly.
  • Sync license state from the provider's current subscription record and reconcile nightly.
Julien Jimenez
Written by

Julien Jimenez

Julien Jimenez is an independent software builder based in Paris. He designs, ships, and operates focused SaaS products for small businesses and independent professionals. Read the full author page.

Issue and validate license keys with one API

Digital product license key issuing and validation API. KeyDispense is built to help you put this into practice.

Get an API key

More from the KeyDispense blog

Get the KeyDispense playbook

Practical guides on license delivery, straight to your inbox as we publish them. No spam, unsubscribe any time.

By subscribing you agree to our privacy policy.