Skip to main content
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.
Pylon creates a hosted Checkout Session for the current user. It accepts Stripe webhooks with full signature verification. Your plugin or app code still owns entitlements, plan limits, and dunning.

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 store stripeCustomerId on the Org entity and route through app handlers.

Schema

User entity needs stripeCustomerId:

Creating a Checkout Session

Response:
The client redirects the browser to 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.
On the first /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:
Set the signing secret on Pylon:
Stripe POSTs events to /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:
Errors: 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

Use 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_SIGNATURE flag. The Stripe-Signature header is HMAC-SHA256-verified using PYLON_STRIPE_WEBHOOK_SECRET and 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/checkout is gated on authenticated session
  • Organizations — for per-org billing, store stripeCustomerId on the Org entity and key your checkout flow on auth.tenantId