Stripe-billing API
Auto-generated from the OpenAPI spec. Run
node docs-site/scripts/fetch-openapi.jsto regenerate.
GET /api/billing/settings
Get Billing Settings
Return current Stripe connection status. Never returns the secret key.
Response includes webhook_configured: boolean — whether a real webhook signing secret is set
(false for an empty value or the dev placeholder). The desktop app uses this to warn a manager
who has recurring membership types but no webhook configured, since renewals/failed
payments/cancellations are only ever learned about via webhook — unlike one-off purchase
activation, which also has a reconcile-on-return safety net (see
POST /checkout-session/{id}/reconcile below).
Responses
- 200 — Successful Response
POST /api/billing/settings
Post Billing Settings
Persists Stripe credentials. Supports two independent request shapes, matching the two separate "Save" actions in the desktop app's Billing tab — one for the always-visible webhook secret field, one for the manual API keys inside "Advanced options".
Request body — provide secret_key (+ publishable_key), or webhook_secret alone, or both.
| Field | Required | Type | Description |
|---|---|---|---|
secret_key | No | string | Validates against Stripe and switches on the full key-setup path below. Requires publishable_key alongside it. |
publishable_key | No | string | Required if, and only if, secret_key is provided. |
webhook_secret | No | string | Provided alone (no secret_key): updates only the webhook signing secret, no Stripe API call, no change to the studio's connection status. Provided alongside secret_key: a manual override for advanced/self-managed setups, skipping the automatic registration below. |
At least one of secret_key or webhook_secret must be provided.
Full key-setup path (secret_key present): validates the key against Stripe, persists both
keys, and — unless webhook_secret was also given as a manual override — automatically registers
a Stripe webhook endpoint against the studio's own tunnel URL, persisting the signing secret it
gets back (no manual Stripe Dashboard step needed in this path). Safe to call repeatedly ("Test
connection"): an existing endpoint for the same URL is reused/updated in place, never duplicated.
Response includes a webhook_endpoint: {configured, created, url} block describing the registered
endpoint when this path runs.
Webhook-only path (webhook_secret present, secret_key absent): persists only the webhook
secret. No Stripe API call is made, and stripe_connected/stripe_account_id are left untouched —
this is for a studio that's already connected (via either path) and just needs to set or replace
its webhook secret, e.g. after following the Stripe webhook setup guide for a studio connected via Connect with Stripe, which can't
auto-register a webhook itself (Stripe restricts OAuth-issued tokens from managing webhook
endpoints).
Responses
- 200 — Successful Response
- 400
STRIPE_SETTINGS_EMPTY— Neithersecret_keynorwebhook_secretwas provided. - 400
STRIPE_TUNNEL_NOT_CONFIGURED— No tunnel URL is configured yet, so there's nothing to register the webhook endpoint against (full key-setup path only). Configure the studio's Cloudflare Tunnel first (Agon Settings → Connectivity). - 502
STRIPE_WEBHOOK_SETUP_FAILED— The Stripe API call to list/create/update the webhook endpoint failed (e.g. an invalid key, rejected by Stripe). The secret key is never echoed back in this error. - 422
STRIPE_PUBLISHABLE_KEY_REQUIRED—secret_keywas provided withoutpublishable_key. - 422 — Validation Error
GET /api/billing/connect/authorize
Start "Connect with Stripe" — a second, easier onboarding path for a non-technical manager, alongside the manual secret-key form above. Manager-only (JWT).
Returns the URL to open in the manager's own OS browser to begin the Stripe Connect OAuth flow. Agon never asks the manager for their Stripe secret key on this path — Stripe handles authentication and account selection on its own hosted page.
Responses
- 200 —
{"authorize_url": "https://connect.stripe.com/oauth/authorize?..."} - 400
STRIPE_CONNECT_NOT_CONFIGURED— the studio hasn't finished initial setup yet (nopublic_studio_id/directory_secret). Finish Agon Settings setup first, or use the manual secret-key form instead.
After connecting this way, the manager still needs to create a webhook endpoint manually in their own Stripe Dashboard — Stripe doesn't let an OAuth-connected account register one automatically. See the studio-manager guide: Stripe Webhook Setup.
Why this needs a relay: Stripe Connect's OAuth token exchange requires Agon's own platform
secret key, which can never be shipped inside a self-hosted Agon install. That one step is
delegated to a narrow-scope Cloudflare Worker relay (see directory-worker/CLAUDE.md §9.3 in the
Agon repo) that holds the platform secret and nothing else — it never sees a studio's own Stripe
key, a card, a charge, or any client data, only authorization code → Stripe account id.
GET /api/billing/connect/return
Not a JSON API endpoint — this is where the relay redirects the manager's own browser after
Stripe authorizes the connection. It renders a small static "connected" or "try again" HTML page
directly (never a redirect back into the Agon desktop app, since there's no reliable deep-link
back into Electron from an arbitrary OS browser). The Agon desktop app itself learns the result by
re-polling GET /api/billing/settings (on window focus, or via a manual "Refresh status" button)
— not by any callback from this page.
POST /api/billing/checkout-session
Create Checkout Session
Create a Stripe Checkout Session for a membership purchase.
Supports both one-off (mode="payment") and recurring (mode="subscription") purchases depending on the membership type.
Accessible by authenticated clients (who may only purchase for themselves) and by managers (who may initiate on behalf of any client).
Request body
| Field | Required | Type | Description |
|---|---|---|---|
client_id | Yes | integer | |
membership_type_id | Yes | integer | |
success_url | Yes | string | |
cancel_url | Yes | string | |
promo_code | No | string | |
gift_card_code | No | string |
Responses
- 200 — Successful Response
- 422 — Validation Error
POST /api/billing/checkout-session/{stripe_session_id}/reconcile
Reconcile Checkout Session
Safety net for a checkout whose webhook never arrived: lets the client (or staff) ask Stripe
directly whether a specific Checkout Session actually completed, and activates the membership if
so. Webhook delivery is the primary mechanism and can silently stop working; a studio connected via
Stripe Connect OAuth may also have no webhook configured at all until one is set up manually (see
GET /connect/authorize above). Safe to call even if the webhook already handled it — idempotent,
never creates a duplicate membership regardless of call order or timing.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
stripe_session_id | path | Yes | string | The Stripe Checkout Session id, returned by POST /api/billing/checkout-session. |
Responses
- 200 —
{"status": "complete" | "open" | "expired", "reconciled": boolean}.reconciled: trueonly when this call actually activated a membership that wasn't already active;falsemeans either it was already complete (webhook got there first) or Stripe reports it's still unpaid. - 404
CHECKOUT_SESSION_NOT_FOUND— unknown session id, or it belongs to a different client than the caller (deliberately the same generic error for both cases — never confirms to the wrong caller that a given session id exists). - 502
STRIPE_API_ERROR— the live Stripe lookup failed. - 503
STRIPE_NOT_CONFIGURED— no Stripe secret key configured for this studio yet.
POST /api/billing/webhook
Stripe Webhook
Stripe webhook receiver. No auth — Stripe signs the payload.
Responses
- 200 — Successful Response
GET /api/billing/members/{client_id}/subscription
Get Subscription Status
Return the most recent Stripe subscription for a client.
Accessible by managers, instructors, or the client themselves.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
client_id | path | Yes | integer |
Responses
- 200 — Successful Response
- 422 — Validation Error
POST /api/billing/members/{client_id}/subscription/cancel
Cancel Subscription
Cancel a client's active subscription at period end. Manager-only.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
client_id | path | Yes | integer |
Responses
- 200 — Successful Response
- 422 — Validation Error
POST /api/billing/members/{client_id}/subscription/cancel/override
Cancel Subscription Override
Cancel a client's subscription locally in the DB without calling Stripe.
Safety valve for when Stripe is unreachable or webhooks fail. Manager-only. Does NOT make any network calls to Stripe.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
client_id | path | Yes | integer |
Responses
- 200 — Successful Response
- 422 — Validation Error