Sell with Stripe
Sell with Stripe
Someone pays → MinMaxKey issues a license key and emails it to the buyer. A refund → the key is revoked. Built in — no separate receiver to run.
Stripe ──checkout.session.completed──▶ MinMaxKey ──issues a license──▶ your dashboard
Stripe ──charge.refunded────────────▶ MinMaxKey ──revokes it────────▶ your dashboard
This page is that loop, set up in two minutes.
Setup (2 minutes, all in the dashboard)
- Create a product + a license type (what the paid key should look like — lifetime/subscription, seat count).
- On the product page, open Payments — Stripe:
- paste your webhook signing secret (
whsec_...), - pick the license type a completed checkout issues, - copy the endpoint URL. - In Stripe (Dashboard → Developers → Webhooks): Add endpoint, paste the
URL, subscribe to
checkout.session.completedandcharge.refunded. The dashboard's webhook signing secret goes inwhsec_....
Only the webhook signing secret is stored — never a Stripe API key.
See it work in one click
The test payment button on the same panel simulates a completed checkout:
a real license is issued under your chosen license type and appears in the
license list (payment intent marked pi_test_...). Devs integrating MinMaxKey
get immediate proof the loop works before real money moves.
Key delivery (email)
Set one email backend and MinMaxKey emails the buyer their license key right after checkout, and a short note when a subscription renews. Resend is the two-minute path — one API key, no mail server, no SPF/DKIM chore:
MMK_RESEND_API_KEY=re_...
MMK_EMAIL_FROM=YourApp <[email protected]>
(Postmark works the same way; classic SMTP still supported.) It's a background task, so the webhook response stays fast, and it silently no-ops if no backend is configured — the loop still works, it just doesn't email. Full setup + troubleshooting: Email delivery.
Events
checkout.session.completed— one-time purchases issue a license (customer email attached). Subscription checkouts defer toinvoice.paid(which carries the price/tier).invoice.paid— subscriptions: the first invoice issues the license; every renewal pushes the license's expiry forward by the license type duration (they paid another 30 days) and fires the outboundlicense.renewedwebhook.charge.refunded— finds that payment's license and revokes it, firinglicense.revoked. Validation stops immediately.- Deliveries are signature-verified (
Stripe-Signature); a forged event is rejected with 400. Every event id is recorded, so Stripe's retries never double-issue or double-extend. Cancellation uses natural expiry — a canceled subscription keeps working until its paid-until date, then the license expires on its own.
Subscriptions & tiers
A monthly subscription works end to end: checkout issues a 30-day license, each renewal extends it by 30 days, refund of the initial payment revokes it. (A refund of a later renewal charge currently no-ops — its payment intent differs from the stored one.)
If you sell tiers (standard vs premium), map each Stripe price to the
MinMaxKey license type it should issue — same Stripe panel, "prices → license
types". The webhook's price.id picks the right license type (kind, seats,
duration), and the issued license carries
metadata: {stripe_price_id, stripe_subscription}. An unmapped price is
rejected loudly (422) rather than silently licensed under the wrong tier. The
product's default license type is the fallback for one-price setups and the
test button.
CLI
mmk-admin payments set 1 --secret whsec_... --license-type-id 2
mmk-admin payments show 1 # config + endpoint URL
mmk-admin payments test 1 # issue a test license
mmk-admin payments list 1 # payment → license map
mmk-admin payments price-set 1 price_1abc --license-type-id 3 # tier: price → license type
mmk-admin payments price-list 1
mmk-admin payments price-remove 1 price_1abc
mmk-admin payments clear 1
Other providers
Stripe is built in. For Lemon Squeezy, Gumroad or Paddle, run the small
reference receiver in examples/stripe/
— the MinMaxKey side is just POST /v1/products/{id}/licenses and
POST /v1/licenses/{id}/revoke. See Webhooks for events
and signature format.