Stripe integration
Keyvane keeps access in sync with your Stripe subscriptions. Stripe keeps charging cards, sending invoices and handling taxes; Keyvane listens to its webhooks and turns them into licenses and entitlements.
Keyvane never needs your Stripe API keys. It only needs the signing secret of one webhook endpoint, and it verifies every event with it.
Setup (5 minutes)
1. Create a webhook endpoint in Stripe
In the Stripe Dashboard, open Developers → Webhooks → Add endpoint. Start in test mode. Use your project's endpoint URL, which is shown on the Stripe page of the Keyvane dashboard:
https://keyvane.dev/v1/webhooks/stripe/<your project id>
Select these events:
checkout.session.completed
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
customer.subscription.paused
customer.subscription.resumed
customer.updated
invoice.paid
invoice.payment_failed
2. Paste the signing secret into Keyvane
On the endpoint page in Stripe, click Reveal signing secret (whsec_…) and paste it on Keyvane's Stripe page. It is stored encrypted.
3. Map Stripe prices to plans
Open Products & plans, edit each paid plan and add its Stripe price IDs (price_…). Add both the monthly and the yearly price if you have them. A price can belong to only one plan.
When a subscription contains a mapped price, its customer gets that plan. If a subscription contains prices of several plans, the highest-ranked plan wins.
4. Tell Keyvane which of your users paid
Keyvane identifies customers by your user ID. Pass it to Stripe in one of these ways:
Payment Links (no code): append client_reference_id to the link you show a signed-in user.
https://buy.stripe.com/test_abc123?client_reference_id=user_42&prefilled_email=alex%40example.com
Checkout Sessions (server-side):
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: 'price_pro_monthly', quantity: 1 }],
client_reference_id: user.id,
subscription_data: { metadata: { keyvane_customer_id: user.id } },
success_url: 'https://yourapp.com/billing?success=1',
cancel_url: 'https://yourapp.com/billing',
});
Subscriptions created with the API: set metadata.keyvane_customer_id on the subscription.
If you pass nothing, Keyvane still creates a license, linked to a customer that is identified only by the Stripe customer ID. That works for license-key products, but entitlement checks by your user ID will not find it.
Testing
Without a Stripe account: the simulator
npm run stripe:simulate -- subscribe --customer user_42 --plan pro
npm run stripe:simulate -- past-due --customer user_42
npm run stripe:simulate -- cancel --customer user_42
The simulator sends correctly signed, realistic events to your running server. See npm run stripe:simulate for all actions.
With Stripe test mode
- Create a product and price in Stripe test mode, map the price ID to a plan in Keyvane.
- Create a Payment Link for that price.
- Open the link with
?client_reference_id=user_42and pay with card4242 4242 4242 4242. - Within seconds,
GET /v1/entitlements?customer=user_42returns the paid plan.
For local development, the Stripe CLI can forward events to your machine:
stripe listen --forward-to localhost:8787/v1/webhooks/stripe/<project id>
The CLI prints its own signing secret (whsec_…). Paste that one into Keyvane while you test locally.
stripe trigger customer.subscription.createdcreates a subscription with a throwaway price that is not mapped to any plan. Keyvane records the event as ignored and tells you which price is missing. Map it and click Reprocess.
How Stripe states become access
| Stripe subscription status | Access |
|---|---|
active, trialing |
✅ The mapped plan |
active with cancel_at_period_end |
✅ Until the period ends, then ❌ (even if the final event never arrives) |
past_due |
✅ During the product's grace period (default 7 days from the first failure), then ❌ |
unpaid |
❌ |
canceled |
❌ immediately (policy immediate), or ✅ until the paid period ends (policy period_end). Subscriptions canceled after failed payments always lose access immediately. |
incomplete, incomplete_expired, paused |
❌ |
When access ends, the customer falls back to the product's default plan (for example, Free). Upgrades and downgrades change the plan of the existing license, and usage already counted this period is kept.
Reliability guarantees
Mistakes in subscription handling cost real money or lock out paying customers, so the sync is built defensively:
- Verified. Every request is checked against the
Stripe-Signatureheader (HMAC-SHA256) with a 5-minute timestamp tolerance. Unsigned or forged requests are rejected with400and never stored. - Stored. Every verified event is saved with its payload and outcome under Stripe → Events.
- Idempotent. Stripe can deliver an event more than once. Duplicates are acknowledged with
200and not applied again. - Atomic. Each event is applied in a database transaction: all of its effects land, or none do.
- Order-safe. Stripe does not guarantee delivery order. Keyvane applies a subscription snapshot only if it is newer than the last one it applied, breaks same-second ties by lifecycle order (
incomplete→active→canceled), and never revives an ended subscription. - Retried. If processing fails, Keyvane responds
500and Stripe retries with backoff for up to three days. You can also Reprocess any failed or ignored event from the dashboard once you have fixed the cause (for example, a missing price mapping). - Time-based without cron. Grace periods, scheduled cancellations and expirations are evaluated at request time, so they take effect even if no further webhook arrives.
Existing subscribers
Keyvane does not import past subscriptions. Existing subscribers are picked up the next time Stripe sends an event for their subscription (at the latest at renewal). To link them sooner, add keyvane_customer_id to each subscription's metadata in Stripe. That sends customer.subscription.updated, which links and syncs the subscription right away.
Limitations in this version
- Only subscriptions are handled. One-time payments (
mode: payment) are acknowledged but ignored; issue licenses for them with the API or dashboard for now. - One Stripe account per project. Use separate projects for test mode and live mode.
- Stripe is the only payment provider today. Others are on the roadmap. Your app talks to Keyvane's entitlements rather than to the provider, so adding one will not change your code.