Documentation Index
Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
Use this file to discover all available pages before exploring further.
For most apps, use
@pylonsync/stripe — declarative stripe({ plans, hooks }) config, canonical Subscription entity, lifecycle hooks (onSubscriptionActivate/Cancel/etc.), URL allowlist auto-derived from PYLON_PUBLIC_URL. This page documents the framework’s binary-native /api/billing/* routes, which are still available for apps that want a bare-bones path.What’s implemented
| Surface | Behavior |
|---|---|
POST /api/billing/checkout | Mint a Stripe Checkout Session URL for the current user. Auto-creates the Stripe Customer on first use. |
POST /api/billing/webhook | Signature-verified Stripe event ingress. Currently logs the event; plugin hook coming. |
- Plan / entitlement state machine — your app code reads webhook events and writes its own state.
- Hosted Billing Portal — there’s no
/api/billing/portalendpoint yet. Apps that need this construct the URL via Stripe’s API directly. - Per-org billing — checkout is keyed on
user_id, nottenantId. For org-scoped subscriptions, your app storesstripeCustomerIdon the Org entity and your handlers route accordingly.
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 given user, Pylon calls Stripe’s API to create a Customer record using the user’s email, then writes the new cus_... id to User.stripeCustomerId. Subsequent calls reuse that id.
Errors:
| Status | Code | Reason |
|---|---|---|
| 401 | AUTH_REQUIRED | Caller isn’t signed in |
| 400 | MISSING_PRICES | priceIds array is empty or absent |
| 501 | STRIPE_NOT_CONFIGURED | PYLON_STRIPE_API_KEY is not set |
| 502 | STRIPE_FAILED | Stripe’s API returned an error (customer create OR checkout create) |
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:
| Status | Code | Reason |
|---|---|---|
| 400 | WEBHOOK_INVALID | Signature verification failed (wrong secret, replayed timestamp, bad payload) |
| 501 | STRIPE_NOT_CONFIGURED / WEBHOOK_NOT_CONFIGURED | One of the env vars is missing |
plugin_hooks.on_billing_event so apps can react to events (flip user’s plan, schedule grace-period jobs, etc.) without forking the framework. Today, apps that need event reaction proxy the webhook through their own handler that verifies + dispatches.
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’s no
WEBHOOK_INSECURE_SKIP_SIGNATUREflag. TheStripe-Signatureheader is HMAC-SHA256-verified usingPYLON_STRIPE_WEBHOOK_SECRETand the request body’s exact bytes. - Replay protection via the timestamp + tolerance check baked into 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