DOCS LLMs

Selling software licenses with Stripe Payment Links

Selling software licenses with Stripe Payment Links

Connect your Stripe account to LicenseSeat and completed, paid checkouts through a linked Stripe Payment Link issue a license key; eligible email delivery sends it to the buyer. A refund revokes it. If the link sells a subscription, every renewal payment extends the license. Nothing to install: the connection is a webhook endpoint you create in your Stripe Dashboard.

Setup takes about five minutes.

Before you start

Link the product to a license plan before testing. Automatic license emails require an eligible LicenseSeat plan, Settings → Organization → Email license keys to my customers enabled, and a valid buyer email. Issuance can succeed without sending an email. Use a separate test organization when you need complete separation: connections in one organization share product mappings by provider.

Read Email delivery, subscription expiry and event recovery for the common prerequisites and recovery procedure. Setup instructions remain available on the connection page after saving.

1. Create the connection in LicenseSeat

In the LicenseSeat dashboard sidebar open Connected platforms, click Connect Your First Platform (or Connect Another), choose Stripe, and give the connection a name. The page shows your webhook URL. Copy it. Keep it private: only paste it into Stripe.

2. Create the webhook endpoint in Stripe

In Stripe, open Workbench (from the Developers menu, or press ~ anywhere in the Dashboard) and go to the Webhooks tab. The direct link is dashboard.stripe.com/webhooks. Click the button to add a destination. Stripe's own pages label it Add destination or Create an event destination; it is the only primary button on that tab.

Stripe asks for the events first and the URL last. Fill the wizard like this:

Step Value
Events from Your account
API version Use a stable snapshot event API version. Basil (2025-03-31) and later require invoice_payment.paid for subscription refund/dispute correlation; re-test before changing versions.
Events checkout.session.completed, checkout.session.async_payment_succeeded, invoice.paid, invoice_payment.paid, charge.refunded, charge.dispute.created
Destination type Webhook endpoint
Endpoint URL your LicenseSeat webhook URL from step 1

Click Continue between pages and Create destination at the end.

checkout.session.completed reports a completed checkout. LicenseSeat waits if its payment is still unpaid; no_payment_required sessions (for example free trials) can issue immediately. invoice.paid fires on every subscription renewal, and that is what extends the license; if you sell yearly or monthly plans and skip this event, licenses expire after the first period and nothing tells you. charge.refunded fires when you refund the payment, and that revokes the license. checkout.session.async_payment_succeeded covers payment methods that settle days later (bank debits, vouchers). charge.dispute.created revokes access for a disputed payment. invoice_payment.paid is retained as payment-to-invoice history: modern subscription refunds and disputes need it, even though it issues no license itself.

Stripe keeps a sandbox and your live account fully separate: each has its own endpoints, signing secrets and Payment Links. Do this in your live account for real sales.

3. Paste the signing secret

When Stripe finishes creating the destination it opens the endpoint's page. Under Signing secret click Reveal secret. The value starts with whsec_. Copy it into the Signing Secret field on the LicenseSeat connection page and save.

Stripe signs every event with this secret, and LicenseSeat rejects anything that is not signed with it. Every endpoint has its own secret, and a sandbox endpoint's secret is different from the live one. If you ever recreate the endpoint in Stripe, or roll its secret, paste the new value here.

Tell LicenseSeat which license plan to issue for each Stripe Payment Link. On the connection page click Link Payment Link, pick the plan, and enter the link's ID:

  • In Stripe open Payment Links, click the link you sell with, and copy its ID. It starts with plink_ (for example plink_1MoC3ULkdIwHu7ixZjtGpVl2).
  • It is not the buy.stripe.com/… address you share with customers. That address does not contain the ID and will not be accepted.
  • One Payment Link maps to one license plan. If you sell a personal and a studio edition, make two Payment Links and link each to its plan.

Only purchases made through a Payment Link issue licenses. A Payment Link's Buy button is generated from the link and sends the same event; test one purchase to confirm it before relying on it. Checkout Sessions you create yourself with the Stripe API carry no Payment Link ID and are ignored.

5. Test it

Start in a Stripe sandbox. Switch to a sandbox in the Dashboard's account picker, create the endpoint with all six events (step 2) and a Payment Link there, then add a second LicenseSeat Stripe connection with that endpoint's signing secret. Link the sandbox Payment Link before buying. Pay with Stripe's test card 4242 4242 4242 4242, any future expiry and any CVC. Verify the license, its owner and email delivery separately; then refund the payment and verify revocation. For subscriptions, also exercise a renewal, a refund of that renewal, and a dispute.

For real sales, repeat the setup in your live Stripe account with live IDs and secrets. A successful sandbox test does not verify live delivery. If you choose to make a live rehearsal purchase, check Stripe's current fees first; do not assume a refund returns processing fees.

Stripe's stripe trigger checkout.session.completed command does not work as a test: the sample session it creates is not tied to a Payment Link, so LicenseSeat ignores it.

What happens on each event

Stripe event LicenseSeat
checkout.session.completed When payment is paid or no_payment_required, issues one license for the plan linked to the session's Payment Link, owned by the email the buyer entered at checkout, and emails the key. Quantity is always one, whatever the buyer chose.
checkout.session.async_payment_succeeded Same as above, for payment methods that confirm later. If the earlier completed event already issued the license, this one is recognised as a duplicate and ignored.
invoice.paid Only when the invoice's reason is subscription_cycle (a renewal): extends the license to the end of the new billing period. The first invoice of a subscription is ignored because Checkout already issued the license.
invoice_payment.paid Retains the payment-to-invoice relationship for modern subscription refunds/disputes; appears as ignored because it does not fulfill a purchase.
charge.refunded Revokes the license issued for that payment and, when the refund includes an email, queues a refund notice. Partial refunds revoke too.
charge.dispute.created Revokes the license bought with the disputed payment.
Anything else Ignored. Subscribing to extra events does no harm.

Good to know

  • Existing perpetual mappings. Subscription purchases still issue on a perpetual plan. Events that identify a subscription show a warning; cancellation does not revoke lifetime access. Existing perpetual licenses remain perpetual on renewal. Choose a finite plan for access that should expire, and review subscription access and recovery before changing existing mappings.

  • Payment Links only. Matching is done on the Payment Link ID that Stripe puts on the Checkout Session. Sessions created any other way (your own Checkout integration, invoices, the Dashboard's manual payments) never issue a license.

  • Subscriptions. Map a recurring price to a finite license plan whose duration matches your billing interval. Checkout initially grants that plan duration, including for free trials; it does not synchronize Stripe trial dates or the first invoice period. Subsequent invoice.paid renewal events use the matching subscription line period end. Cancellation and failed payment do not change the license: it expires at its stored expiry. Configure trial/initial-period access deliberately and verify it in your rehearsal.

  • Refunding a subscription payment revokes. A refund of any invoice of a subscription, the first one or a renewal, is matched to the subscription and revokes its license. If you only meant to refund one period as a courtesy, reissue the license afterwards from LicenseSeat.

  • Chargebacks. A dispute revokes the license the moment Stripe reports it, before the outcome is known. If you win the dispute, reissue the license from LicenseSeat.

  • Delayed payment methods. For bank debits and vouchers Stripe fires checkout.session.completed while the session is still unpaid. LicenseSeat issues nothing then, and issues the license when checkout.session.async_payment_succeeded arrives days later, which is why that event is in the list.

  • Retries and duplicates. Stripe retries a failed delivery for up to three days in live mode and may deliver an event more than once, in any order. LicenseSeat answers immediately, processes in the background, and recognises duplicates by Stripe's event ID and by the Checkout Session, so a purchase is never fulfilled twice. Renewals missing their purchase history have finite automatic retries. An unmatched refund/dispute is ignored but retained; the recovery sweep resumes it only when history resolves to a license. A later purchase also checks retained reversals before emailing an active key. See the recovery guide; an HTTP 200 means receipt, not completed fulfillment.

  • Thank-you page. Optionally, redirect buyers to a LicenseSeat page that shows their key right after paying: on the connection page open Hosted Thank You Page, copy the URL, then in Stripe edit the Payment Link, go to the After the payment tab, choose Don't show confirmation page and paste the URL. The {CHECKOUT_SESSION_ID} part is filled in by Stripe.

Troubleshooting

  • The connection stays "Incomplete". The signing secret is missing, or no Payment Link is linked yet. Both are on the connection page.
  • The event shows as "ignored" with "no active mapping". The session's Payment Link ID does not match any linked link. Open the link in Stripe's Payment Links page and compare its plink_ ID with what you entered. A buy.stripe.com address is not an ID.
  • The event shows as "ignored" and the payment did not come from a Payment Link. Expected. See "Payment Links only" above.
  • Stripe shows a failed delivery with status 400. The signing secret does not match. Open the endpoint in Workbench → Webhooks, click Reveal secret and paste it again. A sandbox secret will not verify live events, and a secret printed by stripe listen will not verify Dashboard-created endpoints.
  • Licenses stop working after the first year or month. The endpoint is not subscribed to invoice.paid. In Workbench → Webhooks open the endpoint, add the event, and the next renewal will extend the license. Renewals that already happened can be fixed by extending the license by hand.
  • A renewal event shows as "failed". The subscription was not created through a linked Payment Link, so there is no license to extend. Issue the license by hand; future renewals still cannot be matched.
  • A refund did not revoke. Open the event in LicenseSeat. On Basil and later, enable invoice_payment.paid and resend both that relationship event and the relevant invoice.paid/Checkout history, then retry the reversal. A missing relationship leaves the reversal ignored with a no-matching-license explanation; it remains recoverable when the relationship arrives. If history is unavailable, revoke manually. A manually issued replacement does not automatically acquire billing provenance.

References

These are Stripe's own pages. If a label or menu in the Stripe Dashboard ever differs from this guide, they are the source of truth: