Skip to content
SaaS4Builders
API Reference

API Conventions

API versioning, authentication, pagination, filtering, error format, data types, and backward-compatibility rules for the SaaS4Builders REST API.

Every endpoint in SaaS4Builders lives under the /api/v1 prefix, returns JSON with snake_case keys, and follows the conventions documented on this page. Read this before consuming any endpoint.


Versioning

All routes are registered under a single version prefix:

/api/v1/...

The API follows strict backward-compatibility rules within a version:

  • No breaking changes within V1 — fields are never removed or renamed
  • Additive changes only — new optional fields may appear in responses at any time
  • Enum values may be extended — new values can be added to existing enums, so treat unknown values gracefully
  • Breaking changes require a new version (/api/v2)

Authentication

SaaS4Builders uses Laravel Sanctum with two authentication modes.

Bearer Token Mode

External clients and API consumers use token-based authentication:

Authorization: Bearer {access_token}

Tokens are obtained via the login or registration endpoints. The system uses an access/refresh token pair — access tokens are short-lived, and the /auth/refresh endpoint issues new ones using a refresh token.

The Nuxt frontend uses cookie-based session authentication automatically. Requests from the configured frontend domain receive session cookies — no manual token management is needed. CSRF tokens are required for state-changing requests.

Public Endpoints

Several endpoints require no authentication:

Endpoint GroupExample
CatalogGET /api/v1/catalog/plans
CurrenciesGET /api/v1/currencies
Public settingsGET /api/v1/docs/settings/public
Invitations (view)GET /api/v1/invitations/{token}
Onboarding startPOST /api/v1/onboarding/start
Auth (login, register)POST /api/v1/auth/login

For full auth flow details, see Authentication.


Pagination

Paginated Lists

Admin and tenant list endpoints return paginated results with metadata:

{
  "data": [
    { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Acme Corp" }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 3,
    "per_page": 25,
    "total": 72
  }
}

A links object with first, last, prev, and next URLs is also present in the response (from Laravel's default paginator), but data and meta are the contractual fields your frontend should rely on.

Query parameters:

ParamTypeDefaultMaxDescription
pageint1Page number
per_pageint25100Items per page

Non-Paginated Lists

Public catalog endpoints and some tenant endpoints return simple lists without pagination metadata:

{
  "data": [
    { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Pro Plan" }
  ]
}

Lists are always wrapped in a top-level data key — the API never returns raw arrays.


Filtering, Sorting & Includes

List endpoints that support dynamic queries use Spatie QueryBuilder with strict whitelisting. Only explicitly allowed filters, sorts, and includes are accepted.

Filtering

Filters use the filter[field] query parameter syntax:

GET /api/v1/admin/plans?filter[pricing_type]=seat&filter[is_active]=true

Filter types available across the API:

TypeSyntaxBehavior
Exact matchfilter[status]=activeWHERE status = 'active'
Partial searchfilter[name]=acmeWHERE name LIKE '%acme%'
Global searchfilter[search]=acmeSearches across multiple fields
Date rangefilter[created_after]=2026-01-01WHERE created_at >= '2026-01-01'

Sorting

Sort with sort=field (ascending) or sort=-field (descending):

GET /api/v1/admin/products?sort=-created_at
GET /api/v1/admin/plans?sort=sort_order,-created_at

Multiple sort fields are comma-separated. If no sort is provided, a sensible default is applied (typically -created_at).

Includes

Load related resources on demand with include:

GET /api/v1/admin/plans?include=product,entitlements,entitlements.feature

Nested relationships use dot notation. Only whitelisted includes are accepted.

Requesting a non-allowed filter, sort, or include returns a 400 Bad Request error. Always refer to the endpoint documentation for the allowed values.

Error Responses

All errors return JSON. The format depends on the error type.

Validation Errors (422)

Returned when request data fails validation rules:

{
  "message": "The plan id field is required.",
  "errors": {
    "plan_id": [
      "The plan id field is required."
    ]
  }
}

The errors object maps field names to arrays of error messages.

Domain Errors (422)

Business rule violations return a stable, machine-readable code alongside the human-readable message:

{
  "message": "This subscription cannot be upgraded from its current status.",
  "code": "subscription_cannot_be_upgraded"
}
Frontend logic should always rely on the code field when present, not the message. The message is human-readable and may change; the code is stable and non-localized.

Domain error codes used across the API include:

CodeContextDescription
subscription_cannot_be_upgradedPlan changeSubscription status does not allow upgrades
proration_not_supportedPlan changeProration cannot be calculated for this plan change
plan_not_available_in_currencyCheckout, plan changePlan has no price in the requested currency
billing_not_readyCheckout, plan changeTenant is missing required billing fields
plan_change_errorPlan changeGeneric plan change failure
PLAN_NOT_AVAILABLE_IN_CURRENCYOnboardingPlan not available in the requested currency
TENANT_CONTEXT_REQUIREDOnboardingRequest requires tenant context
SUBSCRIPTION_CANNOT_BE_CANCELEDAdmin cancelSubscription status does not allow cancellation
Domain error codes are not yet normalized to a single casing convention. Some use snake_case (e.g., billing_not_ready) while others use UPPER_SNAKE_CASE (e.g., PLAN_NOT_AVAILABLE_IN_CURRENCY). Your frontend should match codes case-sensitively.

Standard HTTP Errors

StatusBodyWhen
401{"message": "Unauthenticated."}Missing or invalid auth token/session
403{"message": "Forbidden."}Authenticated but lacking permission
404{"message": "Not found"}Resource does not exist
419{"message": "CSRF token mismatch"}Invalid CSRF token in SPA mode
429{"message": "Too many requests."}Rate limit exceeded (includes Retry-After header)

Rate Limiting

Endpoints are rate-limited to protect the API and external service quotas.

Endpoint GroupLimitNotes
Public catalog60/minuteProducts, plans, features, currencies
Auth login5/minuteBrute-force protection
Auth register10/minute
Password reset5/minuteForgot + reset endpoints
Token refresh10/minute
OAuth10/minuteRedirect + callback + exchange
Onboarding start5/minute
Onboarding (authenticated)10/minuteStatus, retry, complete
Invitations (public)30/minuteView + accept
Avatar upload/delete10/minute
Push subscriptions10/minute
Stripe price operations30/minuteAdmin Stripe sync endpoints
Studio access60/minute

Authenticated tenant and admin endpoints without an explicit throttle use Laravel's default rate limiter.

When rate-limited, the response includes a Retry-After header with the number of seconds to wait.


Data Type Conventions

snake_case Contract

All JSON keys in API requests and responses use snake_case:

{
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "plan_id": "660e8400-e29b-41d4-a716-446655440001",
  "created_at": "2026-03-27T14:30:00.000000Z",
  "cancel_at_period_end": false
}

The Nuxt frontend automatically transforms these to camelCase via the API client layer, and converts outgoing request bodies back to snake_case.

Money

Money values are represented as an integer cent amount plus a currency code. Never floating point.

{
  "amount_cents": 2999,
  "currency": "EUR"
}

Some nested money objects follow the same pattern:

{
  "subtotal": { "amount_cents": 2500, "currency": "EUR" },
  "tax": { "amount_cents": 499, "currency": "EUR" },
  "total": { "amount_cents": 2999, "currency": "EUR" }
}
The proration preview endpoint uses cents as the key name instead of amount_cents. This is the only exception to the standard money format.

Dates and Times

  • Datetime values: ISO-8601 UTC format — 2026-03-27T14:30:00.000000Z
  • Date-only values: YYYY-MM-DD format — 2026-03-27

All datetimes are UTC. If timezone context matters, it is always explicit in the ISO value.

IDs

  • Most entities (products, plans, features, tenants, subscriptions, invoices): UUID strings
  • Users: Integer IDs (auto-increment)
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "user_id": 42
}

Booleans

Strict JSON booleans (true / false), never 1 / 0 or string equivalents.

Enums

Enum values are stable lowercase snake_case strings. The key enums used across the API:

EnumValues
pricing_typeflat, seat, usage
billing_cyclemonthly, yearly, quarterly, weekly
interval_unitday, week, month, year
subscription_statusactive, trialing, past_due, canceled, unpaid, paused, incomplete, incomplete_expired
entitlement_typeboolean, quota
invoice_statusdraft, open, paid, void, uncollectible
Enum values may be extended in future releases. Your frontend should handle unknown values gracefully rather than failing on unexpected strings.

Nullable vs Optional

  • Nullable fields (always present, value can be null): "canceled_at": null
  • Optional fields (may be absent from the response entirely): used for conditional includes like "plan": {...} when an include is requested

The API prefers nullable over optional. Fields that are absent typically indicate an include was not requested.


Tenancy Scoping

API endpoints are organized into three scopes:

Public Endpoints

No authentication, no tenant context. Used for pricing pages and public information.

GET /api/v1/catalog/plans
GET /api/v1/currencies

Tenant Endpoints

Path-based tenant scoping with a full middleware chain:

GET /api/v1/tenant/{tenantId}/subscription
POST /api/v1/tenant/{tenantId}/checkout

Middleware chain: auth:sanctumtenant.resolvetenant.memberonboarding.complete

The {tenantId} in the path is the tenant's UUID. All data returned is scoped to that tenant — cross-tenant data access is impossible.

Admin Endpoints

Platform administration under the /admin prefix:

GET /api/v1/admin/products
GET /api/v1/admin/tenants/{tenant}/subscriptions

Middleware chain: auth:sanctumimpersonation.preventplatform.admin

Admin endpoints require the user to be a platform administrator. Individual actions may require additional granular permissions (e.g., products.view, plans.create).

For details on tenant resolution and data isolation, see Multi-Tenancy.


Impersonation

Platform administrators can impersonate tenant users for support purposes. When impersonation is active:

  • An X-Impersonation-Token header is included in requests
  • The GET /auth/me endpoint returns the impersonation context: actor_admin, effective_user, effective_tenant, and is_impersonating: true
  • Admin endpoints are blocked (impersonation.prevent middleware) — impersonated sessions can only access tenant-scoped endpoints

See Impersonation for the full impersonation flow.


Backward Compatibility

The V1 API guarantees backward compatibility:

  1. Fields are never removed from existing responses
  2. New optional fields may appear at any time — your code should ignore unknown fields
  3. New enum values may be added — handle unknown values gracefully
  4. New endpoints may be added — they do not affect existing ones
  5. Error codes are stable — once introduced, a code string never changes meaning

If a breaking change becomes necessary, it will be introduced in a new API version (/api/v2).


What's Next