JsonFabrica

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.

MethodPathSummary
POST/v1/signupSelf-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/stripeStripe webhook ingestion (raw passthrough to svc-billing)
POST/v1/billing/portalGet a Stripe Billing Portal link for the caller's own tenant
POST/v1/billing/cancelImmediately cancel the caller's own subscription
POST/v1/signupunauthenticated

Self-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

201Signup accepted — redirect the browser to `checkoutUrl` (or the signup was blocked — see `status`)

object

{
  "tenantId": "tenant_a1b2c3",
  "tier": "starter",
  "status": "pending_payment",
  "checkoutUrl": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3"
}
400Validation or template/generation error

ErrorEnvelope

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "\"name\" is required",
    "details": {
      "field": "name"
    }
  }
}
403Rejected (disposable email domain, or enterprise tier requested)

ErrorEnvelope

GET/v1/signup/session/{sessionId}unauthenticated

Poll 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.

ParamInTypeRequiredDescription
sessionIdpathstringYes—

Responses

200Session status (see `status` for which fields are populated)

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"
}
404Unknown/garbage session id
POST/v1/webhooks/stripeunauthenticated

Stripe 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

200Event processed (or already-processed, deduped)

object

400Invalid/missing Stripe signature
502svc-billing unreachable
POST/v1/billing/portal

Get 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

200Billing Portal session created

object

401Missing, malformed, or invalid API key

ErrorEnvelope

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid API key",
    "details": {}
  }
}
404No billing record for the caller's tenant
POST/v1/billing/cancel

Immediately 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

200Subscription canceled

object

400Caller's tenant has no active subscription to cancel

ErrorEnvelope

401Missing, malformed, or invalid API key

ErrorEnvelope

{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing or invalid API key",
    "details": {}
  }
}
404No billing record for the caller's tenant