minmaxkey

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)

  1. Create a product + a license type (what the paid key should look like — lifetime/subscription, seat count).
  2. 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.
  3. In Stripe (Dashboard → Developers → Webhooks): Add endpoint, paste the URL, subscribe to checkout.session.completed and charge.refunded. The dashboard's webhook signing secret goes in whsec_....

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 to invoice.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 outbound license.renewed webhook.
  • charge.refunded — finds that payment's license and revokes it, firing license.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.