---
title: "List Product Components"
method: GET
path: "/v1/product_components"
tags: ["Products"]
---

# List Product Components

`GET /v1/product_components`

## Request

- Base URL: `https://api.withflex.com`
- URL: `https://api.withflex.com/v1/product_components`
- Auth: HTTP bearer

Returns the product components you have registered, newest first.

Only components you have recorded nutrition for are listed: the underlying name
table is shared across accounts, so listing it would return tens of thousands of
rows that belong to nobody.

Results are paginated — the default `limit` is 10, so reading back a full menu
takes several requests. Pass the last object's `component_nutrition_id` as
`starting_after` to fetch the next page, and keep going until a page comes back
shorter than your `limit`.

Rather than paging the whole catalogue to find one ingredient, filter it:
`client_reference_id` returns the single component carrying your own identifier,
`name` matches the shared component name case-insensitively, and `category` and
`canonical_food_key` narrow by those fields. Metadata is filtered with
`metadata[key]=value`, with multiple keys AND-ed. Filters combine with each other
and with the pagination cursors.

## Query parameters

- `limit` integer, nullable: A limit on the number of objects to be returned. Limit can range between 1 and 100, and the default is 10.
- `offset` integer, nullable: Number of objects to skip before returning results.
- `starting_after` string, nullable: A cursor for use in pagination. `starting_after` is a `component_nutrition_id` that defines your place in the list — pass the one from the last object of the previous page to fetch the next.
- `ending_before` string, nullable: A cursor for use in pagination. `ending_before` is a `component_nutrition_id` that defines your place in the list.
- `client_reference_id` string, nullable: Only return the component carrying this `client_reference_id`. The value is unique within your account and mode, so this returns at most one component — it is the direct answer to "have I registered this ingredient already?".
- `category` 'base' | 'protein' | 'topping' | 'premium' | 'dressing': A component's menu category. An enum on the way in and a plain string on the way out, deliberately. `product_component_nutrition.category` is `text` with no CHECK constraint (per the company convention that the backend owns validation), so a row written by a backfill or by raw SQL can hold a value outside this set. Being strict on write keeps bad values out; being tolerant on read means such a row is still retrievable instead of 500ing on serialization. The variants mirror [`flex_core::product_metadata::VALID_CATEGORIES`], which is what the write path actually validates against; `test_component_category_matches_flex_core` pins the two together so a category added there cannot be silently unreachable through the API. - `base`: The bowl's base — a grain, green or other foundation. - `protein`: A protein. - `topping`: A standard topping. - `premium`: A premium topping. - `dressing`: A dressing. The one category the eligibility engine gates differently.
- `canonical_food_key` string, nullable: Only return components carrying this price-book join key.
- `name` string, nullable: Only return components with this name, matched case-insensitively. Names are shared across accounts, so this is the dedupe lookup for a component you have no id for.
- `metadata` object

## Response `200`

An envelope wrapping a list of product component objects.

- MultipleProductComponentsBodyForV1ProductComponent: An envelope wrapping a list of product component objects.
  - `product_components` V1ProductComponent[], required: The list of product components.
    - `component_id` string, required: The component's unique identifier. This is the id `component_items` on `PATCH /v1/products/{id}` accepts.
    - `component_nutrition_id` string, required: The identifier of the nutrition record holding your figures for this component.
    - `name` string, required: The component's name, normalized to lowercase.
    - `shared` boolean, required: Whether the underlying name row is shared across partners rather than owned by you. `true` for everything registered through this endpoint today, which is why the name cannot be changed.
    - `test_mode` boolean, required: Whether these figures belong to test mode.
    - `category` string, required: The component's menu category.
    - `serving_size_g` number, double, nullable: The standard portion, in grams. Not a per-100 g nutrient.
    - `nutrition` ComponentNutritionValues, required: Nutrient values for one component. **Every value is per 100 g of the component**, never per serving — see `nutrition_basis` on the write paths for how per-serving figures are converted on the way in. A `null` means *not supplied*, which is distinct from `0`: the eligibility engine treats a missing nutrient as a reason it cannot produce a split, whereas a zero is a real measurement.
      - `calories` number, double, nullable: Energy, kcal per 100 g.
      - `total_fat_g` number, double, nullable: Total fat, g per 100 g. Informational — the eligibility gate uses saturated fat.
      - `saturated_fat_g` number, double, nullable: Saturated fat, g per 100 g. This is the fat the eligibility gate uses.
      - `trans_fat_g` number, double, nullable: Trans fat, g per 100 g.
      - `carbohydrate_g` number, double, nullable: Carbohydrate, g per 100 g.
      - `fiber_g` number, double, nullable: Fiber, g per 100 g.
      - `protein_g` number, double, nullable: Protein, g per 100 g.
      - `sodium_mg` number, double, nullable: Sodium, mg per 100 g.
      - `added_sugar_g` number, double, nullable: Added sugar, g per 100 g.
    - `vegan` boolean, required: Whether the component is vegan.
    - `gluten_free` boolean, required: Whether the component is gluten-free.
    - `canonical_food_key` string, nullable: The join key into Flex's national price book, or `null` if not yet mapped.
    - `client_reference_id` string, nullable: Your own identifier for this ingredient, or `null` if you did not set one.
    - `metadata` object, nullable: The key-value pairs you stored on this component, or `null` if none.
    - `created_at` string, required: A timestamp encoded as an RFC 3339 / ISO 8601 string (e.g. `2026-06-15T14:30:00Z`).
    - `updated_at` string: A timestamp encoded as an RFC 3339 / ISO 8601 string (e.g. `2026-06-15T14:30:00Z`).

## Other responses

- `401`: Unauthorized
- `403`: Forbidden
- `404`: Not Found
- `409`: Conflict
- `422`: Validation Error
- `429`: Too Many Requests

## Changes

- **2026-11-03** `eec642897a22`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/withflex/apis/flex-api/changes/v1/product_components/get.md)

---

[API](https://skmtc.dev/withflex/apis/flex-api.md) - [All operations](https://skmtc.dev/withflex/apis/flex-api/llms.txt) - [OpenAPI document](https://skmtc.dev/withflex/apis/flex-api/revisions/eec642897a22?raw)
