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:
- A Stripe account connected to your application (Payments → Connect Stripe). Checkout charges the customer through your Stripe account — Licet never holds the funds.
- A plan with a price (Payments → Plan prices). Checkout sells a plan, and Stripe needs to know what to charge for it.
- 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.
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"
}'
| Field | What it is |
|---|---|
plan_id | The plan to sell. Find it on the portal's plans page — the checkout page pre-fills it. |
external_ref | Your 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_url | Where Stripe sends the customer after paying. A page in your app. |
cancel_url | Where 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:
- Verify the signature. Every delivery carries a
Webhook-Signature: t=<unix>,v1=<hex>header, wherev1isHMAC-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. - Dedupe on
id. Delivery is at-least-once; you may see the same event twice. - Fetch the licence and fulfil. The payload carries identifiers only;
GET /v1/licences/{licence_id}(with aread-scoped key) returns the full licence including thelic_key_…your customer's install registers. Correlate to the buyer with your own records for theexternal_refyou passed at checkout, then deliver the key and unlock the product. - 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
| Response | Why |
|---|---|
401 / 403 | Missing or wrong key, or the key lacks the issue scope. |
| Price/plan errors | The plan has no Stripe price mapped — add one under Plan prices. |
409 spend_cap_reached | Your monthly transaction-fee budget is spent; raise it under Account → Spend cap or wait for the period to reset. |