For most apps, use
@pylonsync/stripe: declarative stripe({ plans, hooks }) config, a canonical Subscription entity, lifecycle hooks (onSubscriptionActivate/Cancel/etc.), and a URL allowlist derived from PYLON_PUBLIC_URL. This page documents the lower-level /api/billing/* routes.What’s implemented
Not included
- Plan and entitlement state machine. Your app code reads webhook events and writes its own state.
- Hosted Billing Portal. Apps construct the URL through Stripe’s API directly.
- Per-org billing. Checkout is keyed on
user_id; org-scoped subscriptions storestripeCustomerIdon the Org entity and route through app handlers.
Schema
User entity needsstripeCustomerId:
Creating a Checkout Session
url. Stripe handles card collection, 3DS, etc. On success Stripe redirects back to successUrl.
Fields:
priceIds(required) — array of Stripe Price ids to add as line items.mode—"subscription"(default) or"payment"for one-shot purchases.successUrl— defaults to/billing/success.cancelUrl— defaults to/billing/cancel.
/checkout for a user, Pylon calls Stripe’s API to create a Customer record from the user’s email. It then writes the new cus_... id to User.stripeCustomerId. Later calls reuse that id.
Errors:
Receiving webhooks
In your Stripe dashboard, configure the webhook endpoint:/api/billing/webhook with a Stripe-Signature header. Pylon verifies the HMAC-SHA256 signature against PYLON_STRIPE_WEBHOOK_SECRET and validates the timestamp is within Stripe’s tolerance (defends against replay).
On success Pylon currently logs the event at tracing::info! and returns:
Plugin hook for app code — Pylon’s roadmap includes
plugin_hooks.on_billing_event so apps can react to events (change a user’s plan, schedule grace-period jobs) without changing the framework. Today, apps that need to react proxy the webhook through their own handler that verifies and dispatches it.
Configuration
sk_test_... and whsec_test_... in dev. Stripe’s CLI (stripe listen --forward-to localhost:4321/api/billing/webhook) is the easiest way to test the webhook path locally.
Security guarantees
- Webhook signature verification is mandatory. Pylon refuses to process unverified events. There is no
WEBHOOK_INSECURE_SKIP_SIGNATUREflag. TheStripe-Signatureheader is HMAC-SHA256-verified usingPYLON_STRIPE_WEBHOOK_SECRETand the request body’s exact bytes. - Replay protection comes from the timestamp and tolerance check in Stripe’s signature format.
- Customer id binds to user_id — checkout always pulls the Stripe customer from the caller’s row, never accepts a caller-supplied
customerId. A user can’t trigger checkout against another user’s Stripe customer. - Failed Stripe API calls return 502, not 500 — the caller gets a clear “upstream issue” signal instead of a generic server error.
Where to go next
- Sessions —
/checkoutis gated on authenticated session - Organizations — for per-org billing, store
stripeCustomerIdon the Org entity and key your checkout flow onauth.tenantId