X Enterprises
nuxt-x-billing

Backend requirements

The consumer backend contract for nuxt-x-billing — envelope, data types, read endpoints, Stripe session endpoints, webhook sync, tenancy, and entitlements.

Backend requirements

The integration point of nuxt-x-billing is a consumer-owned backend contract — the layer never talks to Stripe directly. Your backend owns Stripe credentials, checkout/portal session creation, webhook handling, and the subscription table that is the entitlement source of truth. This page summarizes the package's BACKEND-REQUIREMENTS.md; read that file in the repo for the full authoritative contract (data types, payload shapes, implementation notes). The layer's .playground/server/api/billing/* routes are a working mock of this contract.

Conventions

  • Base URL: all paths are relative to NUXT_PUBLIC_X_BILLING_BASE_URL (empty = same origin). Default base path /api/billing; every path overridable per key via xBilling.endpoints.
  • Auth: requests carry Authorization: Bearer <token> when an auth layer provides $getAuthToken. The backend MUST resolve the caller (user, and tenant when tenancy applies) from the token — never trust client-sent ids.
  • Response envelope: every endpoint returns { "success": true, "data": … }; failures return non-2xx and { "success": false, "data": null, "message": "…" }. The client surfaces message in useBilling().error. Raw (unwrapped) payloads are also tolerated.

Read endpoints

All four are GET, envelope-wrapped, scoped to the authenticated user (or tenant — see below):

EndpointReturns
GET /api/billing/subscriptionThe caller's BillingSubscription, or data: null when none (free tier — not an error).
GET /api/billing/plansThe plan catalog (BillingPlan[]) — drives the pricing page and entitlement mapping.
GET /api/billing/invoicesInvoice history, newest first.
GET /api/billing/payment-methodsSaved payment methods — brand + last4 only, never full card numbers.
featureKeys on the plans endpoint and planId on the subscription endpoint are how the client computes hasFeature() / hasPlan() — keep them consistent with what you enforce server-side.

Session endpoints (Stripe)

EndpointBehavior
POST /api/billing/checkoutCreate a Stripe Checkout Session (subscription mode). Request: { priceId, successUrl?, cancelUrl?, tenantId? }. Response: { id, url } — the client redirects to url. Put your internal user/tenant id in the session metadata so the webhook can attribute the subscription.
POST /api/billing/portalCreate a Stripe Billing Portal Session. Request: { returnUrl?, tenantId? }. Response: { url }.
POST /api/billing/subscription/cancel{ immediate: false } → set cancel_at_period_end; true → cancel now. Returns the updated subscription.
POST /api/billing/subscription/resumeClear a pending cancellation. Returns the updated subscription.

Webhook (Stripe → subscription sync)

POST /api/billing/webhooks/stripe — the subscription table you maintain from these events is the entitlement source of truth; the read endpoints serve from it, not from live Stripe API calls.

  1. Verify the signature (stripe.webhooks.constructEvent); reject invalid payloads with 400.
  2. Handle at minimum: checkout.session.completed (create/activate the subscription row), customer.subscription.updated (sync status/plan/period), customer.subscription.deleted (mark canceled), invoice.payment_failed (mark past_due), invoice.paid (append invoice history).
  3. Ack everything else with 200.
  4. Be idempotent — key processed events by event.id.

Tenancy (per-org billing, optional)

When the consumer composes nuxt-x-tenancy and sets xBilling.tenancy.enabled: true:

  • The client sends X-Tenant-ID: <active tenant id> on every request; the header is authoritative — always cross-check it against the caller's membership (403 otherwise). checkout/portal payloads also carry tenantId for convenience.
  • All read endpoints return the subscription/invoices/payment methods of that tenant, not the user's.
  • seats.used should count active members/invitations of the tenant; the client's canInviteMember() compares it to seats.limit.

Without tenancy enabled, no header is sent and everything is user-scoped (standalone mode).

Entitlements

The client computes UI gating as: entitled = subscription.statusentitledStatuses (default ['active','trialing']) AND the subscription's plan (by planId, fallback priceId) lists the feature key / tier. This is a UX mirror only — always enforce entitlements server-side on your own API routes using the same synced subscription table.


AI Context

guide: backend-requirements
package: "@xenterprises/nuxt-x-billing"
use-when: >
  Implementing the billing backend. Eight endpoints under /api/billing
  (4 reads, checkout/portal sessions, cancel/resume, stripe webhook)
  with a { success, data, message? } envelope. The webhook-synced
  subscription table is the entitlement source of truth; X-Tenant-ID
  scopes everything when tenancy composes. Never trust client ids;
  verify webhook signatures; be idempotent.
Copyright © 2026