NutraSoft food manufacturing ERP software
Esc
  • GuideGetting startedThree steps to your first call and your first order, then the two shapes every response takes.
  • GuideAuthentication and keysBearer keys, test and live environments, rotation without an outage, and what each scope grants.
  • GuideTest modeA second NutraSoft account of your own, seeded with realistic data, that you can safely break.
  • PageErrorsEvery error code the API returns, what it means, what to do about it and whether to retry.
  • PageConventionsThe rules every operation follows: money, time, paging, sorting, nulls, PATCH and identifiers.
Menu

Products

Raw materials, packaging, recipes and finished goods in one resource with two families. Created in NutraSoft, updated by one operation for every type.

The Product object

  • idinteger
  • activeboolean
  • allergensarray of objectsnullable

    On GET /products/{product_id} only; null on the list.

    Show 5 fields of allergens
    • allergen_idinteger
    • codestringnullable
    • is_major_allergenboolean
    • namestringnullable
    • sourcestring

      declared: recorded directly against this product.

  • costnumbernullable

    Cost (Gross) per inventory unit. Requires cost:read; null means redacted, not zero. Separate from operations_cost. On an account that updates costs from bills, the next bill receiving the product recalculates it.

  • created_atstringnullable
    • UTC timestamp
  • descriptionstringnullable
  • is_deletedboolean
  • kindstring

    Derived from product_type. Says which of material and recipe is populated.

    • one ofmaterialrecipe
  • materialobjectnullable

    Populated only when kind is material.

    Show 4 fields of material
    • banned_country_idsarray of integer
    • food_idintegernullable

      The nutrition source. Fetch with GET /foods/{food_id}.

    • substitute_product_idsarray of integer
    • supplier_idsarray of integer
  • maximum_on_handnumbernullable

    Max. Stock Level, in inventory units. 0 means no maximum.

Show 16 more attributes
  • minimum_on_handnumbernullable

    Min. Stock Level, in inventory units (see uom_qty). Stock on hand below it counts as low. 0 means no minimum.

  • namestringnullable
  • operations_costnumbernullable

    Operations cost per inventory unit. Requires cost:read.

  • pricenumbernullable

    Selling price per inventory unit. Requires cost:read; null means redacted, not zero.

  • product_codestringnullable
  • product_typestring
    • one ofRAW_MATERIALPACKAGINGMISCELLANEOUSRECIPESUB_RECIPEFINISHED_GOOD
  • recipeobjectnullable

    Populated only when kind is recipe.

    Show 6 fields of recipe
    • byproduct_product_idsarray of integer
    • ingredients_enstringnullable

      The generated ingredient declaration.

    • ingredients_frstringnullable
    • portion_in_gramsnumbernullable
    • qc_checkpoint_idsarray of integer
    • recipe_grams_yieldnumbernullable
  • shelf_lifeintegernullable

    Days the product is usable after receipt. 0 means not set.

  • tag_idsarray of integernullable

    On GET /products/{product_id} only; null on the list.

  • tax1booleannullable

    Whether tax 1 applies to this product. Requires cost:read.

  • tax2booleannullable

    Whether tax 2 applies to this product. Requires cost:read.

  • unit_of_measure_idintegernullable
  • uom_base_conversionnumbernullable

    Second conversion factor. See uom_qty.

  • uom_base_idintegernullable
  • uom_qtynumbernullable

    First conversion factor. One inventory unit is uom_qty * uom_base_conversion base units.

  • updated_atstringnullable
    • UTC timestamp

Product families

Fields each product family carries
FieldMaterialRecipe
product_typeRAW_MATERIAL, PACKAGING, MISCELLANEOUSRECIPE, SUB_RECIPE, FINISHED_GOOD
kindmaterialrecipe
material.banned_country_idsYesNo, material is null
material.food_idThe nutrition source. Fetch with GET /foods/{food_id}.YesNo, material is null
material.substitute_product_idsYesNo, material is null
material.supplier_idsYesNo, material is null
recipe.byproduct_product_idsNo, recipe is nullYes
recipe.ingredients_enThe generated ingredient declaration.No, recipe is nullYes
recipe.ingredients_frNo, recipe is nullYes
recipe.portion_in_gramsNo, recipe is nullYes
recipe.qc_checkpoint_idsNo, recipe is nullYes
recipe.recipe_grams_yieldNo, recipe is nullYes
Allergensallergens[].allergen_idYesYes
Tagstag_idsYesYes
Units of measureunit_of_measure_idYesYes
Product lotslinked by product_idYesYes

Operations

List products

get/productscatalog:read

One table, two families: every row carries kind, material or recipe. allergens, tag_ids, material and recipe are null here: retrieve a product for them.

Query parameters

  • namestringnullable

    Partial match.

  • product_codestringnullable

    Partial match.

  • product_typesarray of stringnullable

    Repeat the parameter to match any of several types.

    • one ofRAW_MATERIALPACKAGINGMISCELLANEOUSRECIPESUB_RECIPEFINISHED_GOOD
  • searchstringnullable

    Free-text match.

  • sortstringnullable

    Comma-separated field:direction pairs, e.g. id:desc. Sortable: created_at, id, name, product_code, product_type, updated_at. id is always added last, so paging is stable.

  • limitinteger

    Rows to return. Maximum 200.

    • >= 1
    • <= 200
    • default 50
  • offsetinteger

    Rows to skip. Maximum 10000: to read further, narrow the filters or page with updated_since.

    • >= 0
    • <= 10000
    • default 0
  • updated_sincestringnullable

    Only rows changed after this instant, as an ISO 8601 timestamp; without an offset it is read as UTC. Tracks the record itself, not its child rows, and deletions are not reported, so reconcile by re-listing from time to time.

    • UTC timestamp
Returns
200 A page of Product objects in data, with pagination.
Errors
400 401 403 429 500 503
GET /products
curl "https://public-api.nutrasoft.ca/v1/products?limit=50" \
  -H "Authorization: Bearer $NUTRASOFT_API_KEY"
Response200
{
  "data": [
    {
      "id": 123,
      "active": true,
      "allergens": null,
      "cost": 12.5,
      "created_at": "2026-09-10T14:30:00Z",
      "description": "string",
      "is_deleted": false,
      "kind": "material",
      "material": {
        "banned_country_ids": [
          123
        ],
        "food_id": 123,
        "substitute_product_ids": [
          123
        ],
        "supplier_ids": [
          123
        ]
      },
      "maximum_on_hand": 12.5,
      "minimum_on_hand": 12.5,
      "name": "Sample name",
      "operations_cost": 12.5,
      "price": 12.5,
      "product_code": "SAMPLE-01",
      "product_type": "RAW_MATERIAL",
      "recipe": null,
      "shelf_life": 10,
      "tag_ids": null,
      "tax1": false,
      "tax2": false,
      "unit_of_measure_id": 123,
      "uom_base_conversion": 12.5,
      "uom_base_id": 123,
      "uom_qty": 12.5,
      "updated_at": "2026-09-10T14:30:00Z"
    }
  ],
  "pagination": {
    "limit": 50,
    "offset": 0,
    "total": 1,
    "has_more": false
  },
  "order": "id:asc"
}

Retrieve a product

get/products/{product_id}catalog:read

Exactly one of material and recipe is populated, as kind says. The bill of materials is not published in v1.

Path parameters

  • product_idintegerrequired

    The product's id.

    • > 0
Returns
200 One Product object in data.
Errors
400 401 403 404 429 500 503
GET /products/{product_id}
curl "https://public-api.nutrasoft.ca/v1/products/123" \
  -H "Authorization: Bearer $NUTRASOFT_API_KEY"
Response200
{
  "data": {
    "id": 123,
    "active": true,
    "allergens": [
      {
        "allergen_id": 123,
        "code": "SAMPLE-01",
        "is_major_allergen": false,
        "name": "Sample name",
        "source": "string"
      }
    ],
    "cost": 12.5,
    "created_at": "2026-09-10T14:30:00Z",
    "description": "string",
    "is_deleted": false,
    "kind": "material",
    "material": {
      "banned_country_ids": [
        123
      ],
      "food_id": 123,
      "substitute_product_ids": [
        123
      ],
      "supplier_ids": [
        123
      ]
    },
    "maximum_on_hand": 12.5,
    "minimum_on_hand": 12.5,
    "name": "Sample name",
    "operations_cost": 12.5,
    "price": 12.5,
    "product_code": "SAMPLE-01",
    "product_type": "RAW_MATERIAL",
    "recipe": null,
    "shelf_life": 10,
    "tag_ids": [
      123
    ],
    "tax1": false,
    "tax2": false,
    "unit_of_measure_id": 123,
    "uom_base_conversion": 12.5,
    "uom_base_id": 123,
    "uom_qty": 12.5,
    "updated_at": "2026-09-10T14:30:00Z"
  }
}

Update a product

patch/products/{product_id}catalog:write· idempotent with Idempotency-Key

The same fields for every product type. Omitted fields are left unchanged; an explicit null clears description and is refused for every other field. Packaging size, units of measure, product code and type cannot be changed. price and cost are per inventory unit and minimum_on_hand and maximum_on_hand are in inventory units. Writing needs only catalog:write; reading price, cost, tax1 and tax2 back needs cost:read. The change is recorded in the product's history in NutraSoft. When a field Shopify or QuickBooks mirrors changes on a product linked to them, the product is updated there too, as a save in NutraSoft does; a failed push does not fail the request. HubSpot is not updated.

Path parameters

  • product_idintegerrequired

    The product's id.

    • > 0

Headers

  • Idempotency-Keystring

    Your own unique string for this request, up to 200 characters. A retry with the same key returns the first response instead of running again.

    • max 200 chars

Request body

  • activebooleannullable
  • costnumbernullable

    Cost (Gross) per inventory unit.

    • >= 0
  • descriptionstringnullable

    Null or an empty string clears it.

    • max 5000 chars
  • maximum_on_handnumbernullable

    Max. Stock Level, in inventory units. 0 means no maximum.

    • >= 0
  • minimum_on_handnumbernullable

    Min. Stock Level, in inventory units. 0 means no minimum.

    • >= 0
  • namestringnullable

    Surrounding spaces are trimmed.

    • min 1 chars
    • max 100 chars
  • pricenumbernullable

    Selling price per inventory unit.

    • >= 0
  • shelf_lifeintegernullable

    Days the product is usable after receipt. Lots already on hand keep their expiration dates.

    • >= 0
  • tax1booleannullable

    Whether tax 1 applies to this product.

  • tax2booleannullable

    Whether tax 2 applies to this product.

Returns
200 One Product object in data.
Errors
400 401 403 404 409 429 500 503
PATCH /products/{product_id}
curl -X PATCH "https://public-api.nutrasoft.ca/v1/products/123" \
  -H "Authorization: Bearer $NUTRASOFT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3f1c6a52-8d0e-4b7a-9c2f-5e8d1b4a7c90" \
  -d '{
  "minimum_on_hand": 40,
  "price": 24.99
}'
Response200
{
  "data": {
    "id": 123,
    "active": true,
    "allergens": [
      {
        "allergen_id": 123,
        "code": "SAMPLE-01",
        "is_major_allergen": false,
        "name": "Sample name",
        "source": "string"
      }
    ],
    "cost": 12.5,
    "created_at": "2026-09-10T14:30:00Z",
    "description": "string",
    "is_deleted": false,
    "kind": "material",
    "material": {
      "banned_country_ids": [
        123
      ],
      "food_id": 123,
      "substitute_product_ids": [
        123
      ],
      "supplier_ids": [
        123
      ]
    },
    "maximum_on_hand": 12.5,
    "minimum_on_hand": 12.5,
    "name": "Sample name",
    "operations_cost": 12.5,
    "price": 12.5,
    "product_code": "SAMPLE-01",
    "product_type": "RAW_MATERIAL",
    "recipe": null,
    "shelf_life": 10,
    "tag_ids": [
      123
    ],
    "tax1": false,
    "tax2": false,
    "unit_of_measure_id": 123,
    "uom_base_conversion": 12.5,
    "uom_base_id": 123,
    "uom_qty": 12.5,
    "updated_at": "2026-09-10T14:30:00Z"
  }
}