Skip to main content

Billing & Webhooks API

Deliberately thin: the billing module's architecture, provider port (Polar.sh + stub), configuration, and testing walkthroughs live in ../backend/billing.md — this page is just the endpoint contract.

Billing admin — /api/billing

BillingController.javaclass-level @PreAuthorize, so both endpoints require system.admin.billing.manage:

MethodPathPermissionPurpose
GET/api/billing/subscriptionsystem.admin.billing.manageCurrent subscription state for this deployment
POST/api/billing/checkoutsystem.admin.billing.manageCreate a provider checkout session; returns the checkout URL only

The checkout response never flips any paid state — subscription state changes arrive exclusively through a verified webhook (below), so a user abandoning checkout leaves nothing half-updated.

PublicBillingController.java — unauthenticated, consumed by the marketing site's pricing page:

MethodPathAuthPurpose
GET/api/public/billing/checkout-linksnone{ "<planCode>": "<provider-hosted checkout URL>", ... } — plans without a configured link are omitted. Dashboard-created URLs only; no secrets, no state.

Provider webhooks — /api/webhooks/{provider}

BillingWebhookController.java

MethodPathAuthPurpose
POST/api/webhooks/{provider}signature, not JWTProvider event intake ({provider} = polar or stub)

This endpoint is deliberately outside JWT auth (providers don't have tokens — it's in the security config's public list). Authenticity comes from the per-provider signature check over the raw request bytes: the body is bound as a raw string precisely so verification sees the exact bytes that were signed, and all inbound headers are handed to the provider adapter. Polar's Standard-Webhooks-style verification (and its secret-format gotcha) is documented in ../backend/billing.md. Invalid signatures are rejected; a verified event updates the local subscription state.

Local testing with the stub provider

With the default dev stack (stub provider active), drive a webhook by hand — the stub accepts unsigned events:

curl -X POST http://localhost:8081/api/webhooks/stub \
-H 'Content-Type: application/json' \
-d '{"type":"subscription.active","plan_code":"growth"}'

# confirm the state flipped (needs a system-admin token — see api/README.md quickstart)
curl -s http://localhost:8081/api/billing/subscription \
-H "Authorization: Bearer $TOKEN"

Other type values the stub understands: subscription.canceled, subscription.revoked, subscription.past_due. Testing against the real Polar sandbox (tunnels, secrets, scopes): ../backend/billing.md.