# Catalog API
Read catalog products, product plans, and related product metadata from the protected Plandalf API.
Source: /docs/api/catalog
Last modified: 2026-06-20
The Catalog API exposes the product records Plandalf uses around offers, pricing, invoices, and checkout context.


> **Current write status**
>

  Product create, update, and delete routes exist in the Laravel resource registration, but the current controller methods and form requests do not implement public write behavior. Treat catalog product writes as dashboard-managed or integration-managed until the API exposes validated write requests.


## Endpoints


  - [List products](#list-products): Read the products for the authenticated user's current organization.
  - [Retrieve a product](#retrieve-a-product): Fetch one product with product features loaded.
  - [List product plans](#list-product-plans): Read the plans attached to a product.
  - [Authentication](/docs/api/authentication): Use the same bearer token header as the rest of the protected API.


## Authentication

Use a bearer API key or OAuth token. The API resolves the current organization from the authenticated user context.

```http title="Protected API headers"
Authorization: Bearer <api_key_or_oauth_token>
Accept: application/json
Content-Type: application/json
```

## List products

<ApiEndpoint
  method="GET"
  path="/api/catalog/products"
  title="List products"
  description="Returns paginated catalog products for the authenticated user's current organization."
  operationId="listCatalogProducts"
>


### Query parameters


- `per_page` (integer; default: 250):
    Maximum number of products per page. The controller reads this as `per_page` and defaults to `250`.


### List catalog products


```bash title="GET /api/catalog/products"
curl "https://api.plandalf.com/api/catalog/products?per_page=100" \
  -H "Authorization: Bearer $PLANDALF_API_KEY" \
  -H "Accept: application/json"
```


### Response fields


- `data` (array):
    Product resources for the current page.


- `links` (object):
    Laravel pagination links for the product collection.


### Product list response


```json title="200 product list"
{
  "data": [
    {
      "id": "prod_01h...",
      "name": "Pro plan",
      "display_name": "Pro plan",
      "description": "Annual access",
      "family": {
        "id": "family_01h...",
        "name": "Membership",
        "lookup_key": "membership"
      },
      "version_number": 1,
      "status": "active",
      "published_at": 1718846400,
      "archived_at": null
    }
  ],
  "links": {
    "first": "https://api.plandalf.com/api/catalog/products?page=1",
    "last": null,
    "prev": null,
    "next": null
  }
}
```


</ApiEndpoint>

## Retrieve a product

<ApiEndpoint
  method="GET"
  path="/api/catalog/products/{product}"
  title="Retrieve a product"
  description="Returns one catalog product. Product features are loaded when the controller resolves the product."
  operationId="retrieveCatalogProduct"
>


### Path parameters


- `product` (string; required):
    Product route key. Use the `id` returned by the product list.


### Retrieve one product


```bash title="GET /api/catalog/products/{product}"
curl "https://api.plandalf.com/api/catalog/products/prod_01h..." \
  -H "Authorization: Bearer $PLANDALF_API_KEY" \
  -H "Accept: application/json"
```


### Product response


```json title="200 product"
{
  "id": "prod_01h...",
  "name": "pro-plan",
  "display_name": "Pro plan",
  "description": "Annual access",
  "family": {
    "id": "family_01h...",
    "name": "Membership",
    "lookup_key": "membership"
  },
  "version_number": 1,
  "features": [],
  "twins": [],
  "status": "active",
  "published_at": 1718846400,
  "archived_at": null
}
```


### Response fields


- `id` (string):
    Public product route key.


- `name` (string | null):
    Internal or source-system product name.


- `display_name` (string | null):
    Customer-facing display name when one is set.


- `description` (string | null):
    Product description copied from the source system or edited in Plandalf.


- `created_at` (integer | null):
    Unix timestamp for product creation.


- `updated_at` (integer | null):
    Unix timestamp for the last product update.


- `family` (object | null):
    Product family metadata when the `productFamily` relation is loaded.


- `version_number` (integer | null):
    Version marker from the catalog product model.


- `features` (array):
    Product feature records when `productFeatures.feature` is loaded by the show controller.


- `twins` (array):
    Linked provider records when external twins are loaded.


- `status` (string | null):
    Product status, such as active or archived.


- `published_at` (integer | null):
    Unix timestamp for publication when the product is published.


- `archived_at` (integer | null):
    Unix timestamp for archival when the product is archived.


</ApiEndpoint>

## List product plans

<ApiEndpoint
  method="GET"
  path="/api/catalog/products/{product}/plans"
  title="List product plans"
  description="Returns the pricing plans attached to a catalog product."
  operationId="listCatalogProductPlans"
>


### Path parameters


- `product` (string; required):
    Product route key.


### List product plans


```bash title="GET /api/catalog/products/{product}/plans"
curl "https://api.plandalf.com/api/catalog/products/prod_01h.../plans" \
  -H "Authorization: Bearer $PLANDALF_API_KEY" \
  -H "Accept: application/json"
```


### Response fields


- `data` (array):
    Plan resources attached to the product.


- `id` (string):
    Public plan route key.


- `object` (string):
    Resource type. Product plan resources return `plan`.


- `status` (string | null):
    Current plan status.


- `type` (string | null):
    Plan type, such as recurring.


- `renew_interval` (string | null):
    ISO 8601 interval string when the plan renews.


- `charges` (array):
    Loaded charge resources for the plan.


### Product plan shape


```json title="200 product plans"
{
  "id": "plan_01h...",
  "object": "plan",
  "status": "active",
  "type": "recurring",
  "display_name": "Annual",
  "description": "Annual access",
  "name": "annual",
  "renew_interval": "P1Y",
  "billing_anchor": "anniversary",
  "invoice_interval": "P1Y",
  "currency_code": "USD",
  "currency_symbol": "$"
}
```


</ApiEndpoint>

## Write behavior


> **Use the dashboard for writes**
>

  The current `ProductsController` has resource methods for `store`, `update`, and `destroy`, but those methods are empty and their form requests currently return `authorize(): false`. Do not build production automation against product write routes yet.


### Read-only catalog flow


```js title="list-products.js"
const response = await fetch("https://api.plandalf.com/api/catalog/products", {
  headers: {
    Authorization: `Bearer ${process.env.PLANDALF_API_KEY}`,
    Accept: "application/json"
  }
});

const products = await response.json();
```

```bash title="list-products.sh"
curl "https://api.plandalf.com/api/catalog/products" \
  -H "Authorization: Bearer $PLANDALF_API_KEY" \
  -H "Accept: application/json"
```


## Related docs

- [API authentication](/docs/api/authentication)
- [Products](/docs/offers/products)
- [Pricing models](/docs/offers/pricing)
- [API reference](/docs/api/reference)