Skip to main content

Subscriptions & Billing

Subscriptions tie an account to a paid plan via your chosen payment gateway. Prime handles the full lifecycle — checkout, webhook processing, upgrades, and cancellations.

How it works​

  1. User initiates checkout → the backend creates a session with the active payment gateway
  2. User completes payment on the gateway's hosted page
  3. The gateway fires a webhook to Prime → account is upgraded to the paid plan
  4. Subscription IDs and customer IDs are stored on the account for future billing events

Payment gateways​

The active gateway is set by PAYMENT_GATEWAY in .env:

GatewayPAYMENT_GATEWAY value
Lemon Squeezylemonsqueezy
Stripestripe

Switching gateways requires only changing PAYMENT_GATEWAY — no code changes needed — but you must also populate that gateway's price/variant IDs on each plan row and register its webhook endpoint. See Lemon Squeezy Setup for full configuration and for the current Stripe caveats.

API endpoints​

MethodEndpointDescription
GET/paymentsList payment history for the account
GET/invoicesList invoices (paginated with limit/offset)
POST/checkout-linkGenerate a checkout URL for a plan (body: { plan_id, interval })
GET/subscription/statusGet the account's current subscription
POST/subscription/cancelCancel the subscription (at period end; { immediately: true } for instant)
POST/subscription/resumeUndo a scheduled cancellation
GET/subscription/portal-urlGet the gateway's hosted billing portal URL
POST/account/upgradeChange the active plan on an existing subscription
POST/admin/account/upgradeManual plan override, bypassing the gateway (master role)
POST/webhooks/lemonsqueezyLemon Squeezy payment webhook receiver
POST/webhooks/stripeStripe payment webhook receiver

Plan upgrades​

POST /account/upgrade (body: { planName }) changes the plan on an existing subscription through the active gateway's changePlan() method — a variant swap on Lemon Squeezy, a subscription-item price swap on Stripe. Proration is handled entirely by the gateway; the server never computes prorated prices.

Eligibility is checked first: a downgrade is rejected when the current team size exceeds the target plan's user limit, and the 400 response carries a violations array ({ resource, current, limit }) the client renders. The account's plan and billing cycle are then updated locally so the new limits apply immediately, and the gateway's subscription_updated webhook confirms the change.

Accounts without an active subscription must go through checkout instead.

Webhook security​

Payment webhooks are verified using the gateway's signature before any account changes are made:

  • Lemon Squeezy — verified with LEMONSQUEEZY_WEBHOOK_SECRET via HMAC-SHA256
  • Stripe — verified with STRIPE_WEBHOOK_SECRET using Stripe's raw-body signature check

See Payment Webhooks for full setup details.

Trials​

New accounts start on the free tier. The expires_at field tracks trial/subscription end dates. Background jobs monitor expiry and send warning emails ACCOUNT_EXPIRY_REMINDER_DAYS days before the deadline.