Skip to content
SaaS4Builders
API Reference

Products & Plans API

Public catalog endpoints and admin CRUD for products, plans, features, entitlements, prices, and currencies in SaaS4Builders.

The catalog is the foundation of the billing system. It defines what you sell (products), how you price it (plans with prices), and what capabilities each plan grants (features and entitlements).

The API exposes two sets of endpoints:

  • Public catalog — No authentication required. Designed for pricing pages. Returns locale-resolved strings and active items only.
  • Admin CRUD — Platform administrator access. Full management of the catalog with translations, filters, and Stripe synchronization.

Public Catalog Endpoints

These endpoints require no authentication, are cached for 60 seconds, and return non-paginated lists wrapped in {"data": [...]}. They are rate-limited to 60 requests per minute.

GET /api/v1/catalog/products

Returns all active products.

Auth: None Rate limit: 60/minute

Response (200):

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "SaaS Platform",
      "slug": "saas-platform",
      "description": "Complete SaaS platform for builders"
    }
  ]
}

The name and description fields are resolved to the current locale based on the Accept-Language header. Only products where is_active = true are returned.


GET /api/v1/catalog/plans

Returns all active plans with their prices and entitlements.

Auth: None Rate limit: 60/minute

Query Parameters:

ParamTypeRequiredDescription
currencystring (3 chars)NoFilter prices by currency code (e.g., EUR). When omitted, all prices are included.

Response (200):

{
  "data": [
    {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "product_id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Pro",
      "description": "For growing teams",
      "slug": "pro",
      "pricing_type": "seat",
      "billing_cycle": "monthly",
      "interval_unit": "month",
      "interval_count": 1,
      "trial_days": 14,
      "sort_order": 2,
      "metadata": null,
      "prices": [
        {
          "id": "770e8400-e29b-41d4-a716-446655440002",
          "currency": "EUR",
          "price_cents": 2999
        },
        {
          "id": "770e8400-e29b-41d4-a716-446655440003",
          "currency": "USD",
          "price_cents": 3299
        }
      ],
      "entitlements": [
        {
          "id": "880e8400-e29b-41d4-a716-446655440004",
          "feature_id": "990e8400-e29b-41d4-a716-446655440005",
          "type": "quota",
          "value": 25,
          "feature": {
            "code": "team-members",
            "name": "Team Members"
          }
        },
        {
          "id": "880e8400-e29b-41d4-a716-446655440006",
          "feature_id": "990e8400-e29b-41d4-a716-446655440007",
          "type": "boolean",
          "value": null,
          "feature": {
            "code": "priority-support",
            "name": "Priority Support"
          }
        }
      ],
      "created_at": "2026-01-15T10:00:00.000000Z",
      "updated_at": "2026-03-20T14:30:00.000000Z"
    }
  ]
}

Plans are sorted by sort_order. Only plans where both the plan and its parent product are active are returned. Each plan eagerly loads its prices and entitlements with feature details.

The billing_cycle field (e.g., monthly, yearly) is a legacy convenience field. The canonical interval is defined by interval_unit + interval_count (e.g., month / 1 for monthly, month / 3 for quarterly).

GET /api/v1/catalog/features

Returns all active features.

Auth: None Rate limit: 60/minute

Response (200):

{
  "data": [
    {
      "id": "990e8400-e29b-41d4-a716-446655440005",
      "code": "team-members",
      "name": "Team Members",
      "description": "Maximum number of team members allowed"
    },
    {
      "id": "990e8400-e29b-41d4-a716-446655440007",
      "code": "priority-support",
      "name": "Priority Support",
      "description": "Access to priority support channel"
    }
  ]
}

The code field is a stable, kebab-case identifier used throughout the system to reference features programmatically. The name and description are locale-resolved.


GET /api/v1/currencies

Returns all active currencies.

Auth: None Rate limit: 60/minute

Response (200):

{
  "data": [
    {
      "code": "EUR",
      "name": "Euro",
      "symbol": "\u20ac",
      "minor_units": 2
    },
    {
      "code": "USD",
      "name": "US Dollar",
      "symbol": "$",
      "minor_units": 2
    },
    {
      "code": "JPY",
      "name": "Japanese Yen",
      "symbol": "\u00a5",
      "minor_units": 0
    }
  ]
}

The minor_units field indicates the number of decimal places for the currency (0 for zero-decimal currencies like JPY, 2 for EUR/USD, 3 for BHD/KWD). This is critical for correctly interpreting price_cents values — for JPY, price_cents: 3000 means 3000 JPY, not 30.00 JPY.

For more on currency handling, see Currency Rules.


Translatable Fields

The catalog supports four locales: English (en), French (fr), Spanish (es), and Italian (it).

Admin endpoints accept and return translations grouped by locale:

{
  "name": {
    "en": "Professional",
    "fr": "Professionnel",
    "es": "Profesional",
    "it": "Professionale"
  },
  "description": {
    "en": "For growing teams",
    "fr": "Pour les equipes en croissance"
  }
}

The English locale (en) is required when creating a resource. Other locales are optional and can be added later via update.

Public and tenant endpoints return a single locale-resolved value:

{
  "name": "Professional",
  "description": "For growing teams"
}

The locale is determined by the Accept-Language header. If the requested locale has no translation, English is used as the fallback.


Admin Product Endpoints

All admin endpoints require auth:sanctum + platform.admin middleware. Individual actions require specific permissions.

GET /api/v1/admin/products

List all products with filtering and pagination.

Auth: Bearer token Permission: products.view

Query Parameters:

ParamTypeDescription
per_pageintItems per page (default: 25, max: 100)
sortstringSort field. Allowed: created_at. Prefix with - for descending.
filter[name]stringPartial match on product name (searches across locales)
filter[is_active]booleanFilter by active status
filter[search]stringGlobal search across product names
includestringComma-separated. Allowed: plans, plansCount

Response (200):

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "slug": "saas-platform",
      "is_active": true,
      "metadata": null,
      "translations": {
        "en": { "name": "SaaS Platform", "description": "Complete SaaS platform" },
        "fr": { "name": "Plateforme SaaS", "description": "Plateforme SaaS complete" }
      },
      "plans_count": 3,
      "created_at": "2026-01-15T10:00:00.000000Z",
      "updated_at": "2026-03-20T14:30:00.000000Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 25,
    "total": 1
  }
}

Default sort is -created_at (newest first).


POST /api/v1/admin/products

Create a new product.

Auth: Bearer token Permission: products.create

Request Body:

{
  "name": {
    "en": "SaaS Platform",
    "fr": "Plateforme SaaS"
  },
  "description": {
    "en": "Complete SaaS platform for builders",
    "fr": "Plateforme SaaS complete pour les createurs"
  },
  "slug": "saas-platform",
  "is_active": true,
  "metadata": null
}
FieldTypeRequiredValidation
nameobjectYesname.en required, string, max 255
descriptionobjectNodescription.en required if present, string, max 65535
slugstringYesUnique, alpha_dash, max 255
is_activebooleanNoDefaults to true
metadataobjectNoArbitrary key-value pairs

Response (201): ProductResource with loaded translations.


GET /api/v1/admin/products/{product}

Permission: products.view — Returns ProductResource with translations and plans count.

PATCH /api/v1/admin/products/{product}

Permission: products.update — Same fields as POST, all optional. Returns updated ProductResource.

DELETE /api/v1/admin/products/{product}

Permission: products.delete — Returns 204 No Content.


Admin Plan Endpoints

GET /api/v1/admin/plans

List all plans with filtering, sorting, and relationship includes.

Auth: Bearer token Permission: plans.view

Query Parameters:

ParamTypeDescription
per_pageintItems per page (default: 25, max: 100)
sortstringAllowed: sort_order, created_at
filter[name]stringPartial match on plan name
filter[is_active]booleanFilter by active status
filter[product_id]UUIDFilter by parent product
filter[pricing_type]enumflat, seat, or usage
filter[billing_cycle]enummonthly, yearly, quarterly, weekly
filter[search]stringGlobal search across plan names
includestringAllowed: product, entitlements, entitlements.feature

Default sort is sort_order (ascending).

Response (200): Paginated PlanResource with fields: id, product_id, slug, pricing_type, billing_cycle, interval_unit, interval_count, trial_days, is_active, sort_order, metadata, translations, entitlements[], prices[], created_at, updated_at.

Admin plan resources include translations (all locales), prices (with stripe_price_id), and entitlements (with nested features) by default — unlike public resources which only show locale-resolved names and omit Stripe IDs.


POST /api/v1/admin/plans

Create a new plan.

Auth: Bearer token Permission: plans.create

Request Body:

{
  "product_id": "550e8400-e29b-41d4-a716-446655440000",
  "name": {
    "en": "Pro Monthly",
    "fr": "Pro Mensuel"
  },
  "slug": "pro-monthly",
  "pricing_type": "seat",
  "billing_cycle": "monthly",
  "trial_days": 14,
  "sort_order": 2,
  "is_active": true,
  "metadata": null
}
FieldTypeRequiredValidation
product_idUUIDYesMust reference an existing product
nameobjectYesname.en required, string, max 255
slugstringYesUnique, alpha_dash, max 255
pricing_typeenumYesflat, seat, or usage
billing_cycleenumYesmonthly, yearly, quarterly, weekly
trial_daysintNoDefaults to 0. Min: 0.
sort_orderintNoDefaults to 0. Min: 0.
is_activebooleanNoDefaults to true
metadataobjectNoArbitrary key-value pairs

Response (201): PlanResource with loaded translations, entitlements, and prices.


GET /api/v1/admin/plans/{plan}

Permission: plans.view — Returns PlanResource with translations, entitlements (including features), and prices.

PATCH /api/v1/admin/plans/{plan}

Permission: plans.update — All fields optional. You can also sync entitlements and prices inline:

{
  "name": { "en": "Pro Plus" },
  "trial_days": 30,
  "entitlements": [
    { "feature_id": "990e8400-...", "type": "quota", "value": 50 },
    { "feature_id": "990e8400-...", "type": "boolean" }
  ],
  "prices": [
    { "currency": "EUR", "price_cents": 4999, "stripe_price_id": "price_1abc123" },
    { "currency": "USD", "price_cents": 5499 }
  ]
}
When entitlements or prices arrays are included, they perform a sync operation — the plan's set is replaced entirely. Omit these fields to leave them unchanged.

Entitlement validation: type: "boolean" must NOT include a value; type: "quota" must include value as a positive integer.

DELETE /api/v1/admin/plans/{plan}

Permission: plans.delete — Returns 204 No Content.

POST /api/v1/admin/plans/{plan}/duplicate

Permission: plans.create — Duplicates the plan including entitlements and prices. Returns the new PlanResource.


Admin Feature Endpoints

GET /api/v1/admin/features

List all features with filtering and pagination.

Auth: Bearer token Permission: features.view

Query Parameters:

ParamTypeDescription
per_pageintItems per page (default: 25, max: 100)
sortstringAllowed: code, created_at
filter[name]stringPartial match on feature name
filter[code]stringPartial match on feature code
filter[is_active]booleanFilter by active status
filter[search]stringGlobal search across feature names

Response (200):

{
  "data": [
    {
      "id": "990e8400-e29b-41d4-a716-446655440005",
      "code": "team-members",
      "is_active": true,
      "is_system": true,
      "metadata": null,
      "translations": {
        "en": { "name": "Team Members", "description": "Maximum number of team members" },
        "fr": { "name": "Membres d'equipe", "description": "Nombre maximum de membres" }
      },
      "created_at": "2026-01-15T10:00:00.000000Z",
      "updated_at": "2026-01-15T10:00:00.000000Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 25,
    "total": 1
  }
}

The is_system flag indicates a built-in feature that cannot be deleted. System features include team-members (used for seat-based billing) and other core features.


POST /api/v1/admin/features

Create a new feature.

Auth: Bearer token Permission: features.create

Request Body:

{
  "code": "api-calls",
  "name": {
    "en": "API Calls",
    "fr": "Appels API"
  },
  "description": {
    "en": "Monthly API call quota",
    "fr": "Quota mensuel d'appels API"
  },
  "is_active": true,
  "metadata": null
}
FieldTypeRequiredValidation
codestringYesUnique, alpha_dash, max 255
nameobjectYesname.en required, string, max 255
descriptionobjectNodescription.en required if present, max 65535
is_activebooleanNoDefaults to true
metadataobjectNoArbitrary key-value pairs

Response (201): FeatureResource with loaded translations.


GET, PATCH, DELETE /api/v1/admin/features/{feature}

  • GET (features.view) — Returns FeatureResource with translations.
  • PATCH (features.update) — All fields optional. Same shape as POST. Returns updated FeatureResource.
  • DELETE (features.delete) — Returns 204 No Content.
System features (is_system: true) cannot be deleted. Attempting to do so returns a 403 error.

Entitlement Endpoints

Entitlements link features to plans. Each entitlement is either a boolean toggle (feature is on/off) or a quota (feature has a numeric limit).

GET /api/v1/admin/plans/{plan}/entitlements

List entitlements for a specific plan.

Auth: Bearer token Permission: entitlements.view

Response (200): Array of EntitlementResource objects with: id, plan_id, feature_id, type, value, feature (nested), created_at, updated_at.

PUT /api/v1/admin/plans/{plan}/entitlements

Replace all entitlements for a plan (sync operation). Permission: entitlements.sync

{
  "entitlements": [
    { "feature_id": "990e8400-...", "type": "quota", "value": 50 },
    { "feature_id": "990e8400-...", "type": "boolean" }
  ]
}

Response (200): Updated PlanResource with loaded entitlements.

DELETE /api/v1/admin/entitlements/{entitlement}

Permission: entitlements.delete — Returns 204 No Content.


Plan Price Endpoints

Each plan can have prices in multiple currencies. Prices can be linked to Stripe Price objects for checkout.

PUT /api/v1/admin/plans/{plan}/prices

Sync all prices for a plan. Replaces the entire price set.

Auth: Bearer token

Request Body:

{
  "prices": [
    {
      "currency": "EUR",
      "price_cents": 2999,
      "stripe_price_id": "price_1abc123"
    },
    {
      "currency": "USD",
      "price_cents": 3299
    }
  ]
}
FieldTypeRequiredValidation
prices[].currencystringYes3-char ISO code, must exist in currencies table, unique per plan
prices[].price_centsintYesMin: 0
prices[].stripe_price_idstringNoMust start with price_, unique per plan

Response (200): PlanPriceResource[]


DELETE /api/v1/admin/plans/{plan}/prices/{price}

Response: 204 No Content


Stripe Price Management

These endpoints manage the link between local plan prices and Stripe Price objects. All are rate-limited to 30 requests per minute.

MethodPathDescription
POST.../prices/{price}/stripe-createCreate a new Stripe Price from a local price. Body: {product_id, nickname?}
POST.../prices/{price}/stripe-linkLink an existing Stripe Price. Body: {stripe_price_id}. Validates currency, interval, amount.
POST.../prices/{price}/stripe-unlinkRemove the Stripe Price link
POST.../prices/stripe-importImport a Stripe Price as a new local price. Body: {stripe_price_id}
GET.../stripe-price-statusCheck sync status for all plan prices

All POST endpoints return the updated PlanPriceResource. The status endpoint returns an array with status per price: linked, missing, mismatch, stripe_missing, inactive, or error.

For more on Stripe integration, see Stripe Integration.


Currency Endpoints

Admin endpoints for managing supported currencies. All require platform.admin middleware.

MethodPathDescription
GET/api/v1/admin/currenciesList all currencies (active and inactive)
GET/api/v1/admin/currencies/catalogBrowse ISO 4217 currencies not yet added
POST/api/v1/admin/currenciesCreate a currency
POST/api/v1/admin/currencies/bulkBulk-create from an array of ISO codes
PATCH/api/v1/admin/currencies/{currency}Update name, symbol, minor_units, or is_active
DELETE/api/v1/admin/currencies/{currency}Delete a currency (204)

Create request body:

{
  "code": "GBP",
  "name": "British Pound",
  "symbol": "\u00a3",
  "minor_units": 2,
  "is_active": true
}
FieldTypeRequiredValidation
codestringYes3 alpha chars, unique
namestringYesMax 100
symbolstringYesMax 10
minor_unitsintYesMust be 0, 2, or 3
is_activebooleanNoDefaults to true

Bulk create accepts {"codes": ["GBP", "CHF", "CAD"]} and returns {created, skipped_existing, invalid}.

For more on multi-currency architecture, see Currency Rules.


What's Next