Billing
Public, unauthenticated billing endpoints: self-serve signup (proxied to svc-auth, which itself calls svc-billing internally to create the Stripe customer/subscription) and the Stripe webhook ingestion passthrough. svc-billing's other routes are internal-only (see `api-docs/openapi-internal-services.yaml`).
Stripe webhook is not customer-callable
POST /v1/webhooks/stripe is listed here for completeness — it's a raw passthrough that only Stripe itself is expected to call (authenticated via a Stripe-Signature header, not an API key). You won't call this endpoint directly as a JsonFabrica customer.
| Method | Path | Summary |
|---|---|---|
| POST | /v1/signup | Self-serve tenant signup (starts an async, Stripe Checkout-based flow) |
| GET | /v1/signup/session/{sessionId} | Poll a Checkout Session's fulfillment status (signup success page) |
| POST | /v1/webhooks/stripe | Stripe webhook ingestion (raw passthrough to svc-billing) |
| POST | /v1/billing/portal | Get a Stripe Billing Portal link for the caller's own tenant |
| POST | /v1/billing/cancel | Immediately cancel the caller's own subscription |
/v1/signupunauthenticatedSelf-serve tenant signup (starts an async, Stripe Checkout-based flow)
Public, unauthenticated. Proxied straight through to svc-auth's `POST /v1/signup`, which validates the request and rejects disposable email domains and the `enterprise` tier (manual sales only), then calls svc-billing internally to create a Stripe Checkout Session (`mode: subscription`) and returns a `checkoutUrl` to redirect the browser to. **No API key is returned by this call** — payment (including any SCA/3DS challenge) happens entirely on Stripe's hosted Checkout page. Every self-serve tier (`starter`, `growth`, `business`) starts with a 30-day free trial; a valid payment card is still required upfront (collected by Stripe), and it is charged automatically once the trial ends. Once payment clears, Stripe redirects the browser to the configured `SIGNUP_SUCCESS_URL` with `?session_id={CHECKOUT_SESSION_ID}` — poll `GET /v1/signup/session/{sessionId}` there to retrieve the issued API key (also emailed to the signup address as the durable/primary channel). Only available when the gateway is booted with `AUTH_SERVICE_URL` set (it is, in `docker-compose.product.yml`).
Request body — object
{
"email": "[email protected]",
"tier": "starter"
}Responses
object
{
"tenantId": "tenant_a1b2c3",
"tier": "starter",
"status": "pending_payment",
"checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3"
}ErrorEnvelope
{
"error": {
"code": "VALIDATION_ERROR",
"message": "\"name\" is required",
"details": {
"field": "name"
}
}
}ErrorEnvelope
/v1/signup/session/{sessionId}unauthenticatedPoll a Checkout Session's fulfillment status (signup success page)
Public, unauthenticated — the Stripe Checkout Session id itself is the capability token (long, Stripe-generated, random, single- purpose). Proxied straight through to svc-auth's `GET /v1/signup/session/{sessionId}`, which proxies to svc-billing. Intended to be polled by the page at `SIGNUP_SUCCESS_URL` (~every 2s while `pending`) after Stripe redirects the browser back with `?session_id={CHECKOUT_SESSION_ID}`. The returned `apiKey` is a **one-time read**: the first successful fetch after fulfillment returns the real key and clears it server-side; any subsequent fetch for the same session returns `apiKey: null` (email is the durable/primary channel regardless of whether the browser ever polls this endpoint). Only available when the gateway is booted with `AUTH_SERVICE_URL` set.
| Param | In | Type | Required | Description |
|---|---|---|---|---|
| sessionId | path | string | Yes | — |
Responses
object
{
"status": "pending"
}{
"status": "fulfilled",
"tenantId": "tenant_a1b2c3",
"tier": "starter",
"apiKey": "sk_live_EXAMPLE_not_a_real_key"
}{
"status": "fulfilled",
"tenantId": "tenant_a1b2c3",
"tier": "starter",
"apiKey": null
}{
"status": "failed",
"reason": "checkout.session.expired"
}/v1/webhooks/stripeunauthenticatedStripe webhook ingestion (raw passthrough to svc-billing)
Public, unauthenticated (Stripe itself is the only expected caller; authenticity is established via `Stripe-Signature`, not an API key). The gateway forwards the request body byte-for-byte (unparsed) and the `Stripe-Signature` header to svc-billing — required for signature verification. Only available when the gateway is booted with `BILLING_SERVICE_URL` set. [VERIFY: full set of handled Stripe event types — see packages/svc-billing/src/webhookHandler.ts.]
Request body: object
Responses
object
/v1/billing/portalGet a Stripe Billing Portal link for the caller's own tenant
Requires `Authorization: Bearer <api-key>`. Mounted the same way as `POST /v1/signup` — proxied straight to svc-auth, which resolves the caller's own tenantId from the API key server-side (never accepted from the request body/path) and calls svc-billing internally to create a one-time Stripe Billing Portal session for that tenant. Open the returned `url` to let the tenant manage payment methods, view invoices, or cancel their subscription themselves (design/self-service-cancellation.md).
Request body — object
{
"returnUrl": "https://jsonfabrica.com/"
}Responses
object
ErrorEnvelope
{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing or invalid API key",
"details": {}
}
}/v1/billing/cancelImmediately cancel the caller's own subscription
Requires `Authorization: Bearer <api-key>`. Mounted the same way as `POST /v1/signup`/`POST /v1/billing/portal` — proxied to svc-auth, which resolves the caller's own tenantId server-side and calls svc-billing internally to cancel that tenant's Stripe subscription immediately (not at period end). There is no request body — the tenant to cancel is always the caller's own, never a body/path parameter (design/self-service-cancellation.md).
Responses
object
ErrorEnvelope
ErrorEnvelope
{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing or invalid API key",
"details": {}
}
}