Skip to main content

Sell a plan with Checkout

This guide takes you from "customer clicks Buy" to "licence lands in your backend". Your app asks Licet for a checkout session, sends the customer to Stripe's hosted payment page, and receives the licence through a webhook when payment completes.

Your app ──POST /checkout-sessions──▶ Licet ──▶ checkout_url
Your app ──redirect──▶ Stripe Checkout (customer pays)
Licet ──licence.created webhook──▶ Your backend (fulfil here)
Stripe ──redirect──▶ your success_url (thank-you page only)

Before you start

You need three things, all set up in the portal:

  1. A Stripe account connected to your application (Payments → Connect Stripe). Checkout charges the customer through your Stripe account — Licet never holds the funds.
  2. A plan with a price (Payments → Plan prices). Checkout sells a plan, and Stripe needs to know what to charge for it.
  3. A webhook endpoint on your backend registered under Payments → Webhook endpoints — this is how the licence reaches you.

The portal's Payments → Checkout & paywall page checks all of this for your application and pre-fills the request below with your real IDs.

Step 1 — Create an API key with the issue scope

Creating checkout sessions issues licences, so the key your backend uses must carry the issue scope. Create one under Application → API keys, tick issue, and copy the secret when it's shown — it's displayed once and never retrievable again.

note

Scopes are literal: a key with only admin cannot call checkout. Give your checkout backend its own issue-scoped key so revoking it later doesn't take down anything else.

Step 2 — Create a checkout session

When your customer chooses a plan, your backend asks Licet for a checkout session:

curl -X POST https://api.licet.app/v1/apps/{app_id}/checkout-sessions \
-H "Authorization: Bearer sk_live_…" \
-H "Content-Type: application/json" \
-d '{
"plan_id": "pln_01ABC…",
"external_ref": "your-customer-id",
"success_url": "https://yourapp.example/thanks",
"cancel_url": "https://yourapp.example/pricing"
}'
FieldWhat it is
plan_idThe plan to sell. Find it on the portal's plans page — the checkout page pre-fills it.
external_refYour identifier for the customer (user ID, account ID…). Licet stores only a hash of it, and hands it back on the webhook so you can match the licence to the buyer.
success_urlWhere Stripe sends the customer after paying. A page in your app.
cancel_urlWhere Stripe sends the customer if they back out. Usually your pricing page.

Step 3 — Read the checkout URL from the response

{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_live_…",
"session_id": "cs_live_…"
}

session_id is Stripe's ID for the session — log it, it's the correlation ID if you ever need to trace a payment.

Step 4 — Redirect the customer to checkout_url

Send an HTTP redirect (or window.location) to checkout_url. The customer pays on Stripe's hosted page — card details never touch your servers or Licet's.

Step 5 — Fulfil from the webhook, not the success page

This is the step that's easy to get wrong. Reaching success_url is not proof of payment — anyone can type that URL into a browser, and a customer can close the tab after paying and never arrive there. Treat the success page as a thank-you and nothing more.

The real signal is the licence.created event delivered to your webhook endpoint moments after payment completes:

{
"id": "evt_01J…",
"type": "licence.created",
"api_version": "2026-06-01",
"created_at": "2026-07-14T12:00:00Z",
"sequence": 1024,
"account_id": "acct_…",
"application_id": "app_…",
"data": {
"object": { "licence_id": "lic_…", "plan_id": "pln_…", "status": "active" }
},
"previous": null
}

In your handler:

  1. Verify the signature. Every delivery carries a Webhook-Signature: t=<unix>,v1=<hex> header, where v1 is HMAC-SHA256(secret, "{t}.{rawbody}") — the same scheme Stripe uses, with the signing secret shown when you created the endpoint. Reject stale timestamps (older than ~5 minutes). Working verification code is in Handling webhooks.
  2. Dedupe on id. Delivery is at-least-once; you may see the same event twice.
  3. Fetch the licence and fulfil. The payload carries identifiers only; GET /v1/licences/{licence_id} (with a read-scoped key) returns the full licence including the lic_key_… your customer's install registers. Correlate to the buyer with your own records for the external_ref you passed at checkout, then deliver the key and unlock the product.
  4. Respond 2xx quickly (within ~5 seconds). Failures are retried with backoff for up to 12 attempts over roughly two days, and every attempt is visible — and replayable — under Payments → Webhook deliveries in the portal.

Step 6 — Test it

The portal's Checkout & paywall page has a Create test checkout button that runs this exact flow against your connected Stripe account. Open the returned session and pay with a Stripe test card such as 4242 4242 4242 4242 (any future expiry, any CVC), then watch the licence.created delivery arrive under Webhook deliveries.

Fees

Licet's fee is 0.5% of each sale, collected automatically via the Stripe application fee when the customer pays. Stripe's own processing fee applies on top, as it would without Licet.

Common errors

ResponseWhy
401 / 403Missing or wrong key, or the key lacks the issue scope.
Price/plan errorsThe plan has no Stripe price mapped — add one under Plan prices.
409 spend_cap_reachedYour monthly transaction-fee budget is spent; raise it under Account → Spend cap or wait for the period to reset.