Swayzio Core API v1 — base path /api/swayzio/v1
Core APIBilling

Billing

Stripe‑backed subscription management for the authenticated account. Every endpoint is owner‑scoped through the Clerk actor — the Stripe customer is resolved from the account, never from request input. Plans are server‑driven (no fixed enum): the live, checkout‑eligible set is returned on each status response as availablePlanIds.

All routes live under https://app.swayzio.com/api/swayzio/v1/billing. Request bodies are validated with .strict() schemas — unknown fields are rejected. returnPath must be an app‑relative path (starts with /, not //).

Endpoints

MethodPathPurposeAuthRequestResponse
POST/v1/billing/checkoutStart a Stripe Checkout session for a planClerk actorcreateBillingCheckoutRequestSchema{ sessionId, url } 201
POST/v1/billing/portalOpen the Stripe customer portalClerk actorcreateBillingPortalRequestSchema{ sessionId, url }
POST/v1/billing/change-planMove to another plan (checkout or in‑place switch)Clerk actorcreateBillingChangePlanRequestSchemabillingChangePlanResponseSchema
POST/v1/billing/cancelCancel at period endClerk actor(none)billingCancelResponseSchema
POST/v1/billing/resumeUndo a pending cancellationClerk actor(none)billingCancelResponseSchema
GET/v1/billing/statusCurrent subscription + entitlement statusClerk actor(query: none)billingStatusResponseSchema

The Stripe webhook (POST /v1/webhooks/stripe, signature‑verified) is documented on the Webhooks page — it is the source of truth for subscription state and is not a caller‑facing endpoint.

Plan ids are not a fixed enum. availablePlanIds on the status response lists the ids the server currently has Stripe prices configured for — render the full pricing ladder from your catalog but only enable checkout for ids in this set. Plan ids are never Stripe price ids.

Errors

Billing errors are normalized to a small set of stable codes:

CodeStatusWhen
validation_error400Request body failed schema validation
<billing_request_error>4xxBad input the service rejects (e.g. unknown plan)
<billing_configuration_error>5xxStripe prices/plans not configured on the server
stripe_unavailable502 / 503Stripe client could not be reached
billing_database_unavailable503DATABASE_URL / billing migrations not present

See Conventions for the shared error envelope and response headers, and Data Contracts for full field detail.

POST /v1/billing/checkout

Creates a Stripe Checkout session for planId and returns the hosted URL to redirect the user to. Returns 201.

curl -s -X POST https://app.swayzio.com/api/swayzio/v1/billing/checkout \
  -H "Authorization: Bearer $SWAYZIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "planId": "pro", "returnPath": "/settings/billing" }'
{
  "sessionId": "cs_live_…",
  "url": "https://checkout.stripe.com/c/pay/cs_live_…"
}

POST /v1/billing/portal

Opens the Stripe customer portal (update payment method, view invoices, manage the subscription). Same response shape as checkout.

curl -s -X POST https://app.swayzio.com/api/swayzio/v1/billing/portal \
  -H "Authorization: Bearer $SWAYZIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "returnPath": "/settings/billing" }'
{ "sessionId": "bps_…", "url": "https://billing.stripe.com/p/session/…" }

POST /v1/billing/change-plan

Moves the account to planId. The response mode tells you what happened:

  • New subscribermode: "checkout" with a url (and sessionId) to redirect to. No subscription exists yet, so checkout creates one.
  • Existing subscribermode: "switched", the subscription is updated in place (no second subscription is created) and the new planId is echoed back.
curl -s -X POST https://app.swayzio.com/api/swayzio/v1/billing/change-plan \
  -H "Authorization: Bearer $SWAYZIO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "planId": "studio", "returnPath": "/settings/billing" }'
// existing subscriber — switched in place
{ "mode": "switched", "planId": "studio" }
// new subscriber — redirect to checkout
{
  "mode": "checkout",
  "sessionId": "cs_live_…",
  "url": "https://checkout.stripe.com/c/pay/cs_live_…"
}

POST /v1/billing/cancel · POST /v1/billing/resume

cancel marks the subscription to cancel at period end (the account keeps access until currentPeriodEnd). resume clears a pending cancellation. Both take no body and return billingCancelResponseSchema.

curl -s -X POST https://app.swayzio.com/api/swayzio/v1/billing/cancel \
  -H "Authorization: Bearer $SWAYZIO_TOKEN"
{
  "status": "active",
  "cancelAtPeriodEnd": true,
  "currentPeriodEnd": "2026-07-10T00:00:00.000Z"
}

GET /v1/billing/status

Returns the account’s billing and entitlement status, plus the configured plan ids. status is one of active, canceled, free, incomplete, past_due, trialing, unpaid.

curl -s https://app.swayzio.com/api/swayzio/v1/billing/status \
  -H "Authorization: Bearer $SWAYZIO_TOKEN"
{
  "customerConfigured": true,
  "planId": "pro",
  "status": "active",
  "stripeCustomerId": "cus_…",
  "stripeSubscriptionId": "sub_…",
  "availablePlanIds": ["free", "pro", "studio"],
  "cancelAtPeriodEnd": false,
  "currentPeriodEnd": "2026-07-10T00:00:00.000Z"
}

When no Stripe customer is attached yet, customerConfigured is false, status is typically free, and the Stripe id fields are omitted.