Skip to main content

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.
Pylon ships a minimal Stripe integration in the binary: mint a hosted Checkout Session for the current user, accept Stripe webhooks with full signature verification. The rest of your billing logic (entitlements, plan limits, dunning) is plugin / app-code territory — Pylon doesn’t try to be a billing service, it gets you to the point where you can react to Stripe events with your own business logic.

What’s implemented

SurfaceBehavior
POST /api/billing/checkoutMint a Stripe Checkout Session URL for the current user. Auto-creates the Stripe Customer on first use.
POST /api/billing/webhookSignature-verified Stripe event ingress. Currently logs the event; plugin hook coming.
What’s NOT in the framework today:
  • Plan / entitlement state machine — your app code reads webhook events and writes its own state.
  • Hosted Billing Portal — there’s no /api/billing/portal endpoint yet. Apps that need this construct the URL via Stripe’s API directly.
  • Per-org billing — checkout is keyed on user_id, not tenantId. For org-scoped subscriptions, your app stores stripeCustomerId on the Org entity and your handlers route accordingly.

Schema

User entity needs stripeCustomerId:
import { entity, field } from "@pylonsync/sdk";

export const User = entity("User", {
  email: field.string().unique(),
  stripeCustomerId: field.string().optional(),  // cus_... — auto-created on first /checkout
  // ...
});

Creating a Checkout Session

curl -X POST https://your-app/api/billing/checkout \
  -H 'Authorization: Bearer pylon_token' \
  -H 'Content-Type: application/json' \
  -d '{
    "priceIds": ["price_1ABCxyz..."],
    "mode": "subscription",
    "successUrl": "https://your-app.com/billing/success",
    "cancelUrl": "https://your-app.com/billing/cancel"
  }'
Response:
{
  "url": "https://checkout.stripe.com/c/pay/cs_test_...",
  "id": "cs_test_..."
}
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 first /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:
StatusCodeReason
401AUTH_REQUIREDCaller isn’t signed in
400MISSING_PRICESpriceIds array is empty or absent
501STRIPE_NOT_CONFIGUREDPYLON_STRIPE_API_KEY is not set
502STRIPE_FAILEDStripe’s API returned an error (customer create OR checkout create)

Receiving webhooks

In your Stripe dashboard, configure the webhook endpoint:
https://your-app.com/api/billing/webhook
Set the signing secret on Pylon:
PYLON_STRIPE_API_KEY=sk_live_...
PYLON_STRIPE_WEBHOOK_SECRET=whsec_...
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:
{ "received": true }
Errors:
StatusCodeReason
400WEBHOOK_INVALIDSignature verification failed (wrong secret, replayed timestamp, bad payload)
501STRIPE_NOT_CONFIGURED / WEBHOOK_NOT_CONFIGUREDOne of the env vars is missing
Plugin hook for app code — Pylon’s roadmap includes 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

PYLON_STRIPE_API_KEY=sk_live_...               # required — enables /checkout
PYLON_STRIPE_WEBHOOK_SECRET=whsec_...          # required — enables /webhook
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’s 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 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/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