---
title: "Update Product Component"
method: PATCH
path: "/v1/product_components/{id}"
tags: ["Products"]
---

# Update Product Component

`PATCH /v1/product_components/{id}`

## Request

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

Updates your nutrition for a product component. Only the fields you supply are
changed.

The component's `name` cannot be changed — the name row is shared with every
other account that registered the same ingredient. Use `clear` to reset a value
to "not supplied"; omitting a field means "leave unchanged", so omission cannot
express erasure.

`metadata` is the one field that merges rather than replaces: the keys you send
are added or overwritten and the rest are left alone, so removing a key means
`clear: ["metadata"]`. Moving a `client_reference_id` onto this component when
another of your components in this mode already carries that value returns
`409`.

## Path parameters

- `id` string, required: The component's `fcomp_` identifier.

## Request body

- ProductComponentBodyForUpdateProductComponentRequest: An envelope wrapping a single product component object.
  - `product_component` UpdateProductComponentRequest, required: Parameters for updating a product component. Only the fields you supply are changed; omitted fields are left unchanged. There is no `name`: the underlying name row is shared with every other partner that registered the same ingredient, so a rename here would edit their data. Register a new component instead. To reset a value to "not supplied", list it in `clear` — omitting it means "leave alone", so omission cannot express erasure. Clearing a field and setting it in the same request is rejected.
    - `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.
    - `nutrition_basis` 'per_100g' | 'as_served': The basis the supplied nutrient values are expressed on. Storage is always per 100 g; this only describes the *request*. `as_served` requires a usable `serving_size_g` to convert by. - `per_100g`: The values are already per 100 g. The default. - `as_served`: The values describe one `serving_size_g` portion as served, and are converted to per 100 g before being stored.
    - `serving_size_g` number, double, nullable: The standard portion of this component, in grams.
    - `nutrition` ComponentNutritionValues: 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, nullable: Whether the component is vegan.
    - `gluten_free` boolean, nullable: Whether the component is gluten-free.
    - `canonical_food_key` string, nullable: The join key into Flex's national price book. Normalized the same way as on create: lowercased, trimmed, hyphens folded into underscores.
    - `client_reference_id` string, nullable: Your own identifier for this ingredient. Still unique within your account and mode, so moving one onto a component that would collide with another of yours is a `409`.
    - `metadata` object, nullable: Key-value pairs to **merge** into the stored metadata: keys you send are added or overwritten, keys you omit are left alone. A merge cannot remove a key, so erasing the object means `clear: ["metadata"]`.
    - `clear` ClearableField[], nullable: Fields to reset to "not supplied".
      - union: A nullable field an update may reset to "not supplied". `category`, `vegan` and `gluten_free` are absent on purpose: they are never null in storage, so they can be changed but not cleared.
        - 'serving_size_g' | 'calories' | 'total_fat_g' | 'saturated_fat_g' | 'trans_fat_g' | 'carbohydrate_g' | 'fiber_g' | 'protein_g' | 'sodium_mg' | 'added_sugar_g' | 'canonical_food_key' | 'client_reference_id'
        - 'metadata': Removes every metadata key. An update *merges* the object it is given, so this is the only way to take a key back off a component.

## Response `200`

An envelope wrapping a single product component object.

- ProductComponentBodyForV1ProductComponent: An envelope wrapping a single product component object.
  - `product_component` V1ProductComponent, required: A component together with the nutrition this partner has recorded for it. Every nutrient value is per 100 g.
    - `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/:id/patch.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)
