Skip to main content
Migrate Already selling? Move your customers to Keylight without re-issuing a single key.
Keylight
Blog
stripe webhooks licensing

How Stripe + Keylight Issues a License: The Webhook Pipeline

6 min read Nicolas Demanez — Founder

The card clears, Stripe says paid, and the customer is watching their inbox. What they do not see is the sequence between that payment confirmation and the license key arriving — signature verification, product resolution, idempotent minting, email delivery. If you have ever built this bridge yourself, you know exactly where it breaks. This post walks through what Keylight does in that gap, step by step, so you know precisely what you are relying on.

What a self-hosted licensing pipeline costs you in hidden complexity

If you build the payment-to-license bridge yourself, the cost is not one thing — it is seven things that all have to be correct at the same time. Miss any of them and the pipeline either mints duplicate keys, delivers nothing, or accepts spoofed events.

The moving parts: a publicly reachable webhook endpoint; Stripe signature verification before you trust any payload; deduplication logic so a retried event does not mint a second key; an Ed25519 key pair and signing code; secure private-key storage; a transactional email provider for delivery; and a customer portal so buyers can retrieve their key after the original email is gone. The error cases — a retry during a ten-second outage, a refund that should revoke a key — are where hand-built pipelines accumulate debt.

The Keylight pipeline, step by step

When you connect your Stripe account to Keylight and map your Stripe products to Keylight license tiers, the issuance pipeline looks like this for every purchase:

1. Customer completes Stripe Checkout. Stripe charges the card and confirms the payment. Keylight is not involved yet — Stripe is the payment processor, and Keylight never sees card data.

2. Stripe fires checkout.session.completed. Stripe sends the event to Keylight’s webhook endpoint for your tenant, including the checkout session metadata that identifies which Keylight product was purchased.

3. Keylight verifies the Stripe signature. Before reading the payload, Keylight verifies the Stripe-Signature header against your configured webhook secret. Events with an invalid or missing signature are rejected immediately. This is the step that prevents spoofed events from reaching your license issuance logic.

4. Keylight resolves the product and license tier. The session metadata carries the Keylight product ID. Keylight looks up which of your products and license tiers the Stripe price maps to, determining what the customer actually purchased — which feature set, which activation limit.

5. Keylight mints an Ed25519-signed lease. Keylight generates a signed license record. The payload encodes the customer, the product, the tier, the activation limit, the features, and the issuance timestamp. The Ed25519 signature covers all of it — any tampering with the payload after issuance produces a signature mismatch that your app catches locally.

A minted lease looks like this:

{
  "id": "lk_01hx9z4bqncktjvx6a2r3p8wy",
  "customerId": "cus_Qk3mN9vTpLx2Zr",
  "productId": "prod_macos_pro",
  "activationLimit": 3,
  "activationCount": 1,
  "features": ["pro", "export"],
  "issuedAt": "2026-05-15T09:12:00Z",
  "expiresAt": null,
  "revoked": false,
  "sig": "..."
}

6. Keylight delivers the key to the customer. The customer receives an email with their license key. They can also retrieve it at any time from the customer portal — useful when the original email is lost, when they move to a new machine months later, or when they need to manage their device activations.

7. Your dashboard reflects the new issuance. The issued key, the customer it belongs to, the activation count, and whether it is active or revoked all appear in your dashboard. You do not run a reconciliation job; the state is updated as part of the issuance pipeline.

The whole sequence — from the checkout.session.completed event arriving to the customer’s email sending — completes within seconds. For a deeper look at how the signed lease format works and how your app verifies it offline, see the license keys feature page.

Idempotency: why the same Checkout session always produces the same key

Stripe’s webhook delivery is at-least-once. That is deliberate: Stripe would rather deliver an event twice than risk losing it. If your endpoint returns a slow response, Stripe will retry. If there is a transient network error between Stripe’s servers and yours, Stripe will retry. This is the behavior you want for reliability — but it creates a problem if your handler mints a new license on every delivery.

The Keylight pipeline is idempotent on the Checkout session ID. The session ID is the natural deduplication key: one Stripe Checkout session represents one purchase, and that purchase should produce exactly one license. If the same session ID arrives twice, the second delivery produces the same license — no new key is minted, no second email goes out, no duplicate activation record is created.

Walk through a concrete case: the checkout.session.completed event arrives, Keylight verifies the signature, mints the license, emails the customer, and returns a successful response. Stripe marks the event delivered. Then — ten minutes later, due to a transient connection reset on the Stripe side — the same event arrives again. Keylight sees the session ID, recognizes it as already fulfilled, and returns success without minting a second key. The customer has one key. The dashboard shows one issuance.

The alternative — a handler that creates a license on every delivery without checking — is subtler to get wrong than it sounds. The idempotency constraint has to be enforced atomically, or two simultaneous retries can both pass the “already done?” check and both proceed to mint. Getting that right in a hand-built handler under concurrent delivery requires careful database writes. Keylight handles it; you do not have to think about it.

For more background on the webhook setup itself and how the payment-to-license boundary works conceptually, see Stripe Webhooks for macOS License Keys.

What you would build yourself, and why most indie devs should not

The honest tradeoff: if you already have a working Stripe integration with custom invoicing logic, complex tax flows, or deep billing configuration, migrating to a separate licensing layer has a real upfront cost. You need to evaluate whether removing the license-issuance code you already have pays off at your current scale. For teams running at volume with mature internal tooling, that calculation might come out differently.

If you are starting fresh — no existing webhook handler, no licensing database — the Keylight integration is a Stripe account connection and a product mapping in the dashboard. There is no webhook endpoint to host, no signature verification to implement, no idempotency logic to write and test. The pipeline is running before you would have finished wiring the raw Stripe webhook.

If your existing system is producing bugs — duplicate keys on retries, keys not delivered after email failures, license state that drifts from Stripe’s billing state after refunds — the migration cost likely pays off quickly. The edge cases are where hand-built systems accumulate technical debt, and they are the cases Keylight’s pipeline is designed to handle correctly.

The framing is not “Keylight instead of Stripe.” Stripe handles payment processing; Keylight handles the licensing layer on top. Your Stripe account stays yours — the customer relationship, the prices, the payout schedule, Stripe’s standard processing rate. Keylight adds the piece that Stripe does not own: which features this customer should have, how many devices they can activate, and whether that is still true after a refund. For a full picture of the Stripe integration, the feature page covers everything Keylight receives and handles from the Stripe lifecycle.

Most indie developers are not in the business of maintaining a licensing backend. The webhook handler, the idempotency logic, the email pipeline, the customer portal — that is infrastructure, not product. Every hour it takes to build and debug is an hour not spent on the app itself. If that framing matches your situation, send us your feedback — we are always interested in where the pipeline could be clearer or what edge cases you have run into.

Frequently asked

How does Keylight know when a Stripe payment succeeds?+

Keylight listens for the checkout.session.completed webhook from Stripe, resolves which Keylight product and license tier the Stripe price maps to, mints a signed lease, and delivers it to the customer — usually within seconds of the payment.

Do I need to write my own Stripe webhook handler?+

No. You connect your Stripe account to Keylight, map your Stripe products to Keylight license tiers in the dashboard, and the issuance pipeline runs automatically. There is no webhook glue you maintain.

What if Stripe retries the webhook? Will my customer get two license keys?+

No. Keylight is idempotent on the Checkout session ID — the same session always produces the same license ID. Retries do not create duplicates.

Ready to ship?

Create your account and start licensing your apps in under a minute. Free forever tier included.

Start Free