Browse the documentation

MCP tools

Products

MCP tools for the product catalog: records, trash, manufacturer and NUPCO links, price history and tender alerts.

A product is a record in your organization's catalog that describes something you sell: its name, NUPCO code, catalogue number, registration details, packing and prices. What you save here is read by the rest of the platform: the product matches new tenders, fills the NUPCO files and quotations, and supplies the VAT rate on each line. A product also has links to manufacturers and to NUPCO codes, and those have their own tools below. This is the same catalog the Products guide describes.

Note:The packing fields describe one hierarchy: unit, then pack, then case. The unit is named in unit_of_measure, the pack in package_type with the units it holds in package_size, and the case in case_type (empty shows Carton) with the units it holds in units_per_case, a count of units, not packs. What one unit holds, such as 100 tests in a vial or 500 mL in a bag, goes in content_quantity and content_unit together; it is not the physical volume in volume. packing_kind sets the product's type, which suggests the level names in the app. packing_uom and units_per_inner are alternative names for unit_of_measure and package_size: the system keeps each pair equal, so use the first name of each pair. The Packing levels guide explains these fields with examples.
Note:Prices depend on the role. Owner, admin, purchasing and viewer see every price. Sales sees the sale price but not the cost, and technical and viewer (no pricing) see no price at all. The tools leave the price fields (cost_price, cost_currency, sale_price_sar) out of every reply a role may not see them in, and ignore them when that role sends them. The limit depends on the plan: 10 products on Starter, 100 on Pro, unlimited on Enterprise. Products in the trash do not count.

A product has one cost price and one sale price, each stated per one level. price_basis says whether they are per unit (unit), per pack (pack), per case (case) or per one unit of what a unit holds (content, such as a price per test). From that one price, pricing from the catalog and pricing by margin work out each tender line's price in the line's own unit: 150 SAR per carton of 1,000 gloves becomes 15 per box of 100 and 0.15 per glove. With no level stated, the price is used as it is on every line, as it was before these fields. So send price_basis with every price, and never divide a price yourself.

ToolWhat it does
products_listList products
products_getGet a product
products_similarProducts with the same NUPCO code
catalog_files_listSearch the document library
products_export_csvExport the whole catalog
products_import_templateDownload the products import template
products_createCreate a product
products_updateUpdate a product
products_bulk_createCreate many products
products_bulk_updateUpdate many products
products_deleteMove a product to the trash
products_trash_listList trashed products
products_restoreRestore a product from the trash
products_purgeDelete products permanently
product_manufacturer_linkLink a product to a manufacturer
product_manufacturer_unlinkUnlink a product from a manufacturer
product_manufacturer_bulk_linkLink many products to a manufacturer
product_manufacturer_set_defaultSet the default manufacturer
product_nupco_linkLink a product to a NUPCO code
product_nupco_unlinkUnlink a product from a NUPCO code
products_sourcing_historyProduct supplier price history
products_historical_pricesProduct NUPCO award history
products_alerts_listList product alerts
product_compliance_letter_pdfGenerate a compliance letter

Reading the catalog

Three tools read the catalog: a searchable list, one product with all its details, and a full export.

products_list

Lists the products in your organization's catalog, newest first, each with links to its photos. It searches with plain words or with the catalog's field syntax, can limit the results to one manufacturer, and adds each product's linked manufacturers when you ask. Products in the trash are not listed.

Read-only · scope products.read · all roles · Pro and Enterprise plans

ParameterTypeRequiredDescription
pageintegerNoPage number, starting at 1.
page_sizeintegerNoResults per page: 20 by default, 100 at most.
qstringNoThe search text. A plain word matches part of the product name, NUPCO code, catalogue number or a linked manufacturer's name. The structured syntax accepts the fields name:, description:, code:, catalogue:, manufacturer: and keywords:, with AND, OR, NOT, parentheses and - to exclude. keywords: matches a whole stored keyword, not part of one. For example manufacturer:acme -gauze. An invalid query is answered with a 400 that says what is wrong.
manufacturer_idintegerNoOnly products linked to this manufacturer. The ID comes from `manufacturers_list`. When no product is linked to it, the page is empty.
include_manufacturersbooleanNotrue adds a manufacturers field to each product: the list of its manufacturers, each with id, name and is_default.
group_bystringNonupco_code groups the matching products by NUPCO code, primary and linked, instead of listing them. The code most products carry comes first and the products with no code come last. A product with two codes appears under both.
{
  "q": "manufacturer:acme -gauze",
  "include_manufacturers": true,
  "page_size": 50
}
Example arguments

Returns: A page of products in data, with total, page, page_size and total_pages. Each product carries its record fields and photo_urls, the links to its photos.

  • With group_by the reply has groups instead of data. Each group has nupco_code (null for the products with no code), product_count, offset and products, and each product in it carries nupco_codes, all its codes. The reply adds total (the products matched), total_codes, products_without_code and total_rows. Pages count rows, not groups, so a group can continue on the next page, where its offset is above zero.
  • Photo links are temporary, so fetch them when you need them and do not store them.
  • Results are filtered by role: price fields do not come back to a role that may not see them (see the introduction).

products_get

Returns one product with all its fields, its photo links and a list of its attachments. Use it when you need the details of a product whose ID you got from the list.

Read-only · scope products.read · all roles · Pro and Enterprise plans

ParameterTypeRequiredDescription
idintegerYesThe product's ID, from products_list.

Returns: The product record, with photo_urls and attachments. Each attachment has category (one of sfda_cert, catalog or other), name, size, uploaded_at and url.

  • The tool answers 404 when the product does not exist or is in the trash. To find a product in the trash, use `products_trash_list`.
  • Photo and attachment links are temporary, so fetch them when you need them and do not store them.

products_similar

Lists the other products in your catalog that carry one of this product's NUPCO codes, its primary code or a linked one. It is the card on the product page, and the same products the grouped list shows under the code.

Read-only · scope products.read · all roles · Pro and Enterprise plans

ParameterTypeRequiredDescription
idintegerYesThe product's ID, from products_list.
{
  "id": 42
}
Example arguments

Returns: codes, the product's own codes with the primary first, and products, most shared codes first. Each product carries its record fields, shared_codes, nupco_codes, photo_urls and manufacturers.

  • Codes are compared after trimming and normalizing their spelling, so a code as Excel mangles it matches the original.
  • Results are filtered by role: price fields do not come back to a role that may not see them (see the introduction).

catalog_files_list

Searches your organization's document library in one list: product photos and attachments (SFDA certificate, catalog, other), manufacturer attachments (catalog, SFDA AR license, ISO certificate, authorization letter) and the documents filed on sales orders (the customer's purchase order). It is the Document library page of the app.

Read-only · scope products.read · all roles · Pro and Enterprise plans

ParameterTypeRequiredDescription
qstringNoThe words to find, up to 200 characters. Every word must match somewhere in the product's name, primary or linked NUPCO code, catalogue number, linked manufacturer names, a sales order's number, customer or customer PO number, the file's name or its type, so syringe 5ml cert narrows the results instead of widening them.
scopestringNoall (the default), products for product files only, manufacturers for manufacturer files only, or sales_orders for sales order documents only.
categorystringNoOne file type: photo, sfda_cert, catalog, other, sfda_ar, iso, authorization or purchase_order (a customer's purchase order). catalog covers both product and manufacturer catalogs.
manufacturer_idintegerNoOnly this manufacturer's files and the files of the products linked to it. The ID comes from `manufacturers_list`.
pageintegerNoPage number, starting at 1.
page_sizeintegerNoFiles per page: 50 by default, 100 at most.
{
  "q": "syringe cert",
  "scope": "products",
  "page_size": 20
}
Example arguments

Returns: A page of files in data, with total, page, page_size and total_pages. Each file has owner_kind (product, manufacturer or sales_order), owner_id, owner_name, nupco_code, catalogue_number (a sales order's customer PO number), customer_name, manufacturer_names, category, name, size, mime_type, uploaded_at and url.

  • Manufacturer files appear only when the key also has the manufacturers.read scope, and sales order documents only when it has orders.read and its owner's role may open sales order documents (every role but Technical and Purchasing). Open what a file belongs to with `products_get`, manufacturers_get or `sales_orders_get`.
  • Links are temporary and expire after an hour, so fetch them when you need them and do not store them.

products_export_csv

Exports your organization's whole catalog, every row rather than one page, as CSV text that Claude can read in the reply itself. Owner and admin only.

Read-only · scope export · roles: owner, admin · Pro and Enterprise plans

No parameters.

Returns: CSV text in UTF-8 with a byte order mark at the start: a header row, then one row per product. It is text in the reply, not a file.

  • The headers are in English and match the labels that `sheet_import` reads, so the file can be edited and imported back.
  • A price that was never entered is an empty cell, not a zero. A semicolon (;) separates manufacturers, and separates keywords, within one cell.
  • Photo links are not exported because they expire.

products_import_template

Downloads the Excel template for importing products: a Products sheet with the import's main columns, and an instructions sheet that explains each column with an example and lists every further column the import accepts. Hand it to the catalog's owner to fill in.

Read-only · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
localestringNoThe language of the instructions sheet and the header notes: ar or en, en by default. The headers stay English in both.
{
  "locale": "ar"
}
Example arguments

Returns: The xlsx file encoded in base64, with its name, type and size.

  • The template's headers are the import's own, so a filled copy given to `sheet_import` with the products target is mapped from its headers with no detection step. It holds no example row, since an example left on the sheet would be imported as a product.
  • Pricing columns the key owner's role may not see are left out of the template.

Creating and changing products

Creating and changing work one product at a time or in batches. The single-product tools accept every field; the batch tools accept a smaller set.

products_create

Adds a product to your organization's catalog. The name is the only required field, and the rest can be filled in later. A nupco_product_code is also added to the product's NUPCO code links. The tool answers with a 402 when your organization has reached its plan's product limit.

Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
product_namestringYesThe product's name, 1 to 200 characters.
descriptionstringNoA free-form description, up to 2,000 characters.
keywordsstring[]NoKeywords for searching the catalog (keywords:); they do not match tenders. Up to 20, each up to 100 characters. The list you send replaces the stored list as a whole.
nupco_product_codestringNoThe product's primary NUPCO material code, up to 100 characters. It is also added to the product's NUPCO code links.
search_querystringNoA saved tender search query that alerts match against, up to 500 characters, in the same syntax as the tender search page. A product never matches tenders by its name or its keywords: it matches through this query or its linked NUPCO codes, and a product with neither matches nothing. A structured query that cannot be parsed, names an unknown field or has no positive term is refused.
agency_filterstringNoText the buying agency's name must contain for a match to count, up to 200 characters.
weight_kgnumberNoUnit weight in kg, 0 to 99,999.999.
length_cmnumberNoUnit length in cm, 0 to 99,999.99.
width_cmnumberNoUnit width in cm, 0 to 99,999.99.
height_cmnumberNoUnit height in cm, 0 to 99,999.99.
units_per_caseintegerNoUnits in one case, a whole number from 1 to 1,000,000. It counts units, not packs: 10 boxes of 100 is 1,000.
case_typestringNoThe name of the case, the outer level of the unit, pack and case hierarchy, such as Carton, Box or Drum, up to 50 characters. Empty shows Carton. The name is kept as you send it.
case_weight_kgnumberNoCase weight in kg, 0 to 99,999.999.
case_length_cmnumberNoCase length in cm, 0 to 99,999.99.
case_width_cmnumberNoCase width in cm, 0 to 99,999.99.
case_height_cmnumberNoCase height in cm, 0 to 99,999.99.
units_per_innerintegerNoAn alternative name for package_size (units in one pack), whole numbers only. The system keeps the two equal, so prefer package_size.
inner_length_cmnumberNoLength in cm of the pack that package_size describes, 0 to 99,999.99.
inner_width_cmnumberNoPack width in cm, 0 to 99,999.99.
inner_height_cmnumberNoPack height in cm, 0 to 99,999.99.
unit_of_measurestringNoThe name of one unit, the smallest level of the unit, pack and case hierarchy, for example Piece. Up to 50 characters.
country_of_origin_codestringNoThe country of origin as a two-letter uppercase ISO 3166-1 code, such as SA or DE.
catalogue_numberstringNoThe manufacturer's catalogue number, up to 100 characters.
mdma_statusstringNoThe MDMA registration status: ready once the authorization is issued, so submission packs include the SFDA certificate, or under_registration while the application is pending, so packs carry the registration status letter the system generates instead.
listing_numberstringNoThe SFDA listing number, up to 200 characters.
min_order_qtyintegerNoThe smallest quantity the supplier accepts per order, a whole number from 1 to 1,000,000.
mdma_numberstringNoThe SFDA Medical Device Marketing Authorization number, up to 200 characters. It is separate from mdma_status (the state of the authorization) and listing_number (the listing number).
mdma_expirystringNoThe expiry date of the MDMA authorization, as YYYY-MM-DD.
shelf_life_yearsnumberNoThe product's shelf life in years, fractions allowed, above 0 and up to 9,999.99.
first_delivery_daysintegerNoLead time for the first delivery, in days, 0 to 3,650. When it is empty the product inherits your organization's default.
order_delivery_daysintegerNoLead time for later orders, in days, 0 to 3,650. When it is empty the product inherits your organization's default.
volumenumberNoThe volume of the sellable unit, in the unit named by volume_unit, for example 500.
volume_unitstringNoThe unit of the volume, such as ml or L. Up to 50 characters.
content_quantitynumberNoWhat one unit (unit_of_measure) holds, in content_unit: 100 for a vial of 100 tests, 500 for a 500 mL bag. Send it together with content_unit, or neither. It is not the physical volume in volume, which stays the NUPCO file's Volume column.
content_unitstringNoThe unit of content_quantity, one of test, reaction, dose, tablet, capsule, ml, l, gal (US gallon), mcg, mg, g, kg, iu, u, cm or m.
package_typestringNoThe name of the pack the product is sold in (the NUPCO file's Package type), such as Box or Bottle. Up to 100 characters.
package_sizenumberNoUnits in one pack (the NUPCO file's Package size), for example 100. It may be fractional, such as 2.5 for a product measured in litres.
packing_uomstringNoAn alternative name for unit_of_measure (the unit's name). The system keeps the two equal, so prefer unit_of_measure.
packing_kindstringNoThe product's type, which presets its packing names in the app: a broad category (item, liquid, tablet, lab_test, or service for installation, delivery and other services: no packing, medical or registration facts, the unit is a job, visit, hour, day, month or lump sum, and the price is used as it is on every tender line) or a narrower kind (gloves, tablets, injectables, liquids, iv_fluids, syringes, sutures, dressings, rolls, lab_reagent, lab_control, test_devices or devices).
foc_percentnumberNoThe free-of-charge (bonus) percentage offered with the product, 0 to 100. It is informational in the product's info file and is not used in any calculation.
vat_percentnumberNoThis product's VAT rate in percent, 0 to 100, written on each row of the NUPCO response files. Send 0 for medical products that qualify for zero-rating. null means the standard 15.
additional_infostringNoFree text for the additional information line of the product's info file, up to 2,000 characters.
price_basisstringNoThe level cost_price and sale_price_sar are stated per: unit (one unit_of_measure), pack (package_size units), case (units_per_case units) or content (one content_unit of what a unit holds). Without it the prices are used as they are on every tender line. Send it whenever you send a price. Each level needs its count: pack a whole package_size of at least 1, case units_per_case, content content_quantity and content_unit. null clears it, and clearing it on a product with a saved price needs reprice: "keep".
cost_pricenumberNoThe cost price in the currency of cost_currency, 0 to 9,999,999.99, for one price_basis level.
cost_currencystringNoThe currency of cost_price: USD (US dollar) or SAR (Saudi riyal). Defaults to USD.
sale_price_sarnumberNoThe sale price in SAR, 0 to 9,999,999.99, for one price_basis level.
is_medicalbooleanNoWhether the product is a medical device or supply. Defaults to false. It decides the compliance letter's type when the MDMA status does not.
{
  "product_name": "Disposable syringe 5 ml, Luer lock",
  "nupco_product_code": "4111610529900",
  "catalogue_number": "SYR-5-LL",
  "keywords": [
    "syringe",
    "luer lock"
  ],
  "unit_of_measure": "Piece",
  "package_type": "Box",
  "package_size": 100,
  "units_per_case": 1000,
  "country_of_origin_code": "DE",
  "is_medical": true,
  "vat_percent": 0,
  "price_basis": "pack",
  "cost_price": 12,
  "cost_currency": "USD",
  "sale_price_sar": 55
}
Example arguments

Returns: The new product in a 201 reply, with all its fields, its new id and an empty photo_urls.

  • When the limit is reached, the 402 reply carries error: plan_limit_reached, limit and current.
  • Fields your role cannot see are ignored on write, so sales does not save cost_price or cost_currency.
  • Marking a product medical with this tool does not change vat_percent. Set it yourself: 0 for medical products that qualify for zero-rating.
  • Send price_basis with the prices, and the level's count with it: price_basis: "case" with units_per_case. A level missing its count is refused with a 400 (price_basis_level_missing) that names the field, and content_quantity without content_unit with a 400 (content_incomplete).
  • To import a list from an Excel or CSV file, use `sheet_import` with the products target.

products_update

Changes fields of a product; only what you send changes. Send null to clear a field that can be empty. A new NUPCO code is added to the product's links, and the old links stay until you remove them with `product_nupco_unlink`.

Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
idintegerYesThe product's ID, from products_list.
product_namestringNoThe product's new name, 1 to 200 characters.
descriptionstringNoA free-form description, up to 2,000 characters. Send null to clear it.
keywordsstring[]NoKeywords for searching the catalog (keywords:); they do not match tenders. Up to 20, each up to 100 characters. The list you send replaces the stored list as a whole.
nupco_product_codestringNoThe product's primary NUPCO material code, up to 100 characters. It is also added to the product's NUPCO code links. Send null to clear it.
search_querystringNoA saved tender search query that alerts match against, up to 500 characters, in the same syntax as the tender search page. A product never matches tenders by its name or its keywords: it matches through this query or its linked NUPCO codes, and a product with neither matches nothing. A structured query that cannot be parsed, names an unknown field or has no positive term is refused. Send null to clear it.
agency_filterstringNoText the buying agency's name must contain for a match to count, up to 200 characters. Send null to clear it.
weight_kgnumberNoUnit weight in kg, 0 to 99,999.999. Send null to clear it.
length_cmnumberNoUnit length in cm, 0 to 99,999.99. Send null to clear it.
width_cmnumberNoUnit width in cm, 0 to 99,999.99. Send null to clear it.
height_cmnumberNoUnit height in cm, 0 to 99,999.99. Send null to clear it.
units_per_caseintegerNoUnits in one case, a whole number from 1 to 1,000,000. It counts units, not packs: 10 boxes of 100 is 1,000. Send null to clear it.
case_typestringNoThe name of the case, the outer level of the unit, pack and case hierarchy, such as Carton, Box or Drum, up to 50 characters. Empty shows Carton. The name is kept as you send it. Send null to clear it.
case_weight_kgnumberNoCase weight in kg, 0 to 99,999.999. Send null to clear it.
case_length_cmnumberNoCase length in cm, 0 to 99,999.99. Send null to clear it.
case_width_cmnumberNoCase width in cm, 0 to 99,999.99. Send null to clear it.
case_height_cmnumberNoCase height in cm, 0 to 99,999.99. Send null to clear it.
units_per_innerintegerNoAn alternative name for package_size (units in one pack), whole numbers only. The system keeps the two equal, so prefer package_size. Send null to clear it.
inner_length_cmnumberNoLength in cm of the pack that package_size describes, 0 to 99,999.99. Send null to clear it.
inner_width_cmnumberNoPack width in cm, 0 to 99,999.99. Send null to clear it.
inner_height_cmnumberNoPack height in cm, 0 to 99,999.99. Send null to clear it.
unit_of_measurestringNoThe name of one unit, the smallest level of the unit, pack and case hierarchy, for example Piece. Up to 50 characters. Send null to clear it.
country_of_origin_codestringNoThe country of origin as a two-letter uppercase ISO 3166-1 code, such as SA or DE. Send null to clear it.
catalogue_numberstringNoThe manufacturer's catalogue number, up to 100 characters. Send null to clear it.
mdma_statusstringNoThe MDMA registration status: ready once the authorization is issued, so submission packs include the SFDA certificate, or under_registration while the application is pending, so packs carry the registration status letter the system generates instead. Send null to clear it.
listing_numberstringNoThe SFDA listing number, up to 200 characters. Send null to clear it.
min_order_qtyintegerNoThe smallest quantity the supplier accepts per order, a whole number from 1 to 1,000,000. Send null to clear it.
mdma_numberstringNoThe SFDA Medical Device Marketing Authorization number, up to 200 characters. It is separate from mdma_status (the state of the authorization) and listing_number (the listing number). Send null to clear it.
mdma_expirystringNoThe expiry date of the MDMA authorization, as YYYY-MM-DD. Send null to clear it.
shelf_life_yearsnumberNoThe product's shelf life in years, fractions allowed, above 0 and up to 9,999.99. Send null to clear it.
first_delivery_daysintegerNoLead time for the first delivery, in days, 0 to 3,650. When it is empty the product inherits your organization's default. Send null to clear it.
order_delivery_daysintegerNoLead time for later orders, in days, 0 to 3,650. When it is empty the product inherits your organization's default. Send null to clear it.
volumenumberNoThe volume of the sellable unit, in the unit named by volume_unit, for example 500. Send null to clear it.
volume_unitstringNoThe unit of the volume, such as ml or L. Up to 50 characters. Send null to clear it.
content_quantitynumberNoWhat one unit (unit_of_measure) holds, in content_unit: 100 for a vial of 100 tests, 500 for a 500 mL bag. Send it together with content_unit, or neither. It is not the physical volume in volume, which stays the NUPCO file's Volume column. Send null to clear it.
content_unitstringNoThe unit of content_quantity, one of test, reaction, dose, tablet, capsule, ml, l, gal (US gallon), mcg, mg, g, kg, iu, u, cm or m. Send null to clear it.
package_typestringNoThe name of the pack the product is sold in (the NUPCO file's Package type), such as Box or Bottle. Up to 100 characters. Send null to clear it.
package_sizenumberNoUnits in one pack (the NUPCO file's Package size), for example 100. It may be fractional, such as 2.5 for a product measured in litres. Send null to clear it.
packing_uomstringNoAn alternative name for unit_of_measure (the unit's name). The system keeps the two equal, so prefer unit_of_measure. Send null to clear it.
packing_kindstringNoThe product's type, which presets its packing names in the app: a broad category (item, liquid, tablet, lab_test, or service for installation, delivery and other services: no packing, medical or registration facts, the unit is a job, visit, hour, day, month or lump sum, and the price is used as it is on every tender line) or a narrower kind (gloves, tablets, injectables, liquids, iv_fluids, syringes, sutures, dressings, rolls, lab_reagent, lab_control, test_devices or devices). Send null to clear it.
foc_percentnumberNoThe free-of-charge (bonus) percentage offered with the product, 0 to 100. It is informational in the product's info file and is not used in any calculation. Send null to clear it.
vat_percentnumberNoThis product's VAT rate in percent, 0 to 100, written on each row of the NUPCO response files. Send 0 for medical products that qualify for zero-rating. null means the standard 15.
additional_infostringNoFree text for the additional information line of the product's info file, up to 2,000 characters. Send null to clear it.
price_basisstringNoThe level cost_price and sale_price_sar are stated per: unit (one unit_of_measure), pack (package_size units), case (units_per_case units) or content (one content_unit of what a unit holds). Without it the prices are used as they are on every tender line. Send it whenever you send a price. Each level needs its count: pack a whole package_size of at least 1, case units_per_case, content content_quantity and content_unit. null clears it, and clearing it on a product with a saved price needs reprice: "keep".
cost_pricenumberNoThe cost price in the currency of cost_currency, 0 to 9,999,999.99, for one price_basis level. Send null to clear it.
cost_currencystringNoThe currency of cost_price: USD (US dollar) or SAR (Saudi riyal). Defaults to USD.
sale_price_sarnumberNoThe sale price in SAR, 0 to 9,999,999.99, for one price_basis level. Send null to clear it.
is_medicalbooleanNoWhether the product is a medical device or supply. Defaults to false. It decides the compliance letter's type when the MDMA status does not.
repricestringNoWhat happens to the saved prices you do not resend when you change a stated price_basis: convert restates them at the new level (150 per case of 1,000 becomes 15 per pack of 100), keep keeps the numbers as the price per the new level. It is required for that change, and clearing price_basis takes keep only. It is not needed when no level was stated before, or when you send the new prices yourself. It is never stored.
{
  "id": 1207,
  "price_basis": "pack",
  "reprice": "convert",
  "package_size": 100
}
Example arguments

Returns: The product after the change, with photo_urls.

  • The tool answers 404 when the product does not exist or is in the trash.
  • A call that carries no field your role may write is refused with a 400 (No fields to update).
  • Sending a new nupco_product_code adds a link and does not remove the old one, and sending null clears the field and leaves the links as they are.
  • Marking a product medical here does not change vat_percent: set it yourself.
  • Stating price_basis on a product that had none does not change the numbers: it only says what they meant. A price you send is taken for the level as the same call leaves it. A role that cannot see the cost price (sales) cannot change price_basis or send reprice: the server ignores them from it.
  • The tool refuses with a 400 and writes nothing, and the message names the field and the path that works: price_basis_level_missing when the stated level's count is empty (set package_size, units_per_case or content_quantity in the same call, or choose another level), content_incomplete when one of content_quantity and content_unit comes without the other, reprice_required when a stated level changes or is cleared on a product with a saved price you do not resend (send reprice or the new prices), and converted_price_out_of_range when a converted price would pass 9,999,999.99 (send the prices yourself or reprice: "keep").

products_bulk_create

Creates up to 500 products in one call. The product rows are saved together: if any check fails, such as an unknown manufacturer or the plan limit, nothing is created. You can link every new product to one manufacturer, or name manufacturers per row: names are matched to your own manufacturers ignoring case, and a name you do not have yet is created.

Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
productsobject[]YesThe product rows to create, from 1 to 500.
products[].product_namestringYesThe product's name, 1 to 200 characters.
products[].descriptionstringNoA free-form description, up to 2,000 characters.
products[].nupco_product_codestringNoThe product's primary NUPCO material code, up to 100 characters. It is also added to the product's NUPCO code links.
products[].catalogue_numberstringNoThe manufacturer's catalogue number, up to 100 characters.
products[].cost_pricenumberNoThe cost price in the currency of cost_currency, 0 to 9,999,999.99, for one price_basis level.
products[].cost_currencystringNoThe currency of cost_price: USD (US dollar) or SAR (Saudi riyal). Defaults to USD.
products[].sale_price_sarnumberNoThe sale price in SAR, 0 to 9,999,999.99, for one price_basis level.
products[].units_per_caseintegerNoUnits in one case, a whole number from 1 to 1,000,000. It counts units, not packs: 10 boxes of 100 is 1,000.
products[].is_medicalbooleanNoWhether the product is a medical device or supply. Defaults to false. It decides the compliance letter's type when the MDMA status does not.
products[].price_basisstringNoThe level cost_price and sale_price_sar are stated per: unit (one unit_of_measure), pack (package_size units), case (units_per_case units) or content (one content_unit of what a unit holds). Without it the prices are used as they are on every tender line. Send it whenever you send a price. Each level needs its count: pack a whole package_size of at least 1, case units_per_case, content content_quantity and content_unit. null clears it, and clearing it on a product with a saved price needs reprice: "keep".
products[].case_typestringNoThe name of the case, the outer level of the unit, pack and case hierarchy, such as Carton, Box or Drum, up to 50 characters. Empty shows Carton. The name is kept as you send it.
products[].content_quantitynumberNoWhat one unit (unit_of_measure) holds, in content_unit: 100 for a vial of 100 tests, 500 for a 500 mL bag. Send it together with content_unit, or neither. It is not the physical volume in volume, which stays the NUPCO file's Volume column.
products[].content_unitstringNoThe unit of content_quantity, one of test, reaction, dose, tablet, capsule, ml, l, gal (US gallon), mcg, mg, g, kg, iu, u, cm or m.
products[].packing_kindstringNoThe product's type, which presets its packing names in the app: a broad category (item, liquid, tablet, lab_test, or service for installation, delivery and other services: no packing, medical or registration facts, the unit is a job, visit, hour, day, month or lump sum, and the price is used as it is on every tender line) or a narrower kind (gloves, tablets, injectables, liquids, iv_fluids, syringes, sutures, dressings, rolls, lab_reagent, lab_control, test_devices or devices).
products[].search_querystringNoA saved tender search query that alerts match against, up to 500 characters, in the same syntax as the tender search page. A product never matches tenders by its name or its keywords: it matches through this query or its linked NUPCO codes, and a product with neither matches nothing. A structured query that cannot be parsed, names an unknown field or has no positive term is refused.
products[].unit_of_measurestringNoThe name of one unit, the smallest level of the unit, pack and case hierarchy, for example Piece. Up to 50 characters.
products[].country_of_origin_codestringNoThe country of origin as a two-letter uppercase ISO 3166-1 code, such as SA or DE.
products[].mdma_statusstringNoThe MDMA registration status: ready once the authorization is issued, so submission packs include the SFDA certificate, or under_registration while the application is pending, so packs carry the registration status letter the system generates instead.
products[].listing_numberstringNoThe SFDA listing number, up to 200 characters.
products[].vat_percentnumberNoThis product's VAT rate in percent, 0 to 100, written on each row of the NUPCO response files. Send 0 for medical products that qualify for zero-rating. null means the standard 15.
products[].weight_kgnumberNoUnit weight in kg, 0 to 99,999.999.
products[].length_cmnumberNoUnit length in cm, 0 to 99,999.99.
products[].width_cmnumberNoUnit width in cm, 0 to 99,999.99.
products[].height_cmnumberNoUnit height in cm, 0 to 99,999.99.
products[].inner_length_cmnumberNoLength in cm of the pack that package_size describes, 0 to 99,999.99.
products[].inner_width_cmnumberNoPack width in cm, 0 to 99,999.99.
products[].inner_height_cmnumberNoPack height in cm, 0 to 99,999.99.
products[].case_weight_kgnumberNoCase weight in kg, 0 to 99,999.999.
products[].case_length_cmnumberNoCase length in cm, 0 to 99,999.99.
products[].case_width_cmnumberNoCase width in cm, 0 to 99,999.99.
products[].case_height_cmnumberNoCase height in cm, 0 to 99,999.99.
products[].min_order_qtyintegerNoThe smallest quantity the supplier accepts per order, a whole number from 1 to 1,000,000.
products[].mdma_numberstringNoThe SFDA Medical Device Marketing Authorization number, up to 200 characters. It is separate from mdma_status (the state of the authorization) and listing_number (the listing number).
products[].mdma_expirystringNoThe expiry date of the MDMA authorization, as YYYY-MM-DD.
products[].shelf_life_yearsnumberNoThe product's shelf life in years, fractions allowed, above 0 and up to 9,999.99.
products[].first_delivery_daysintegerNoLead time for the first delivery, in days, 0 to 3,650. When it is empty the product inherits your organization's default.
products[].order_delivery_daysintegerNoLead time for later orders, in days, 0 to 3,650. When it is empty the product inherits your organization's default.
products[].volumenumberNoThe volume of the sellable unit, in the unit named by volume_unit, for example 500.
products[].volume_unitstringNoThe unit of the volume, such as ml or L. Up to 50 characters.
products[].package_typestringNoThe name of the pack the product is sold in (the NUPCO file's Package type), such as Box or Bottle. Up to 100 characters.
products[].package_sizenumberNoUnits in one pack (the NUPCO file's Package size), for example 100. It may be fractional, such as 2.5 for a product measured in litres.
products[].packing_uomstringNoAn alternative name for unit_of_measure (the unit's name). The system keeps the two equal, so prefer unit_of_measure.
products[].foc_percentnumberNoThe free-of-charge (bonus) percentage offered with the product, 0 to 100. It is informational in the product's info file and is not used in any calculation.
products[].additional_infostringNoFree text for the additional information line of the product's info file, up to 2,000 characters.
products[].manufacturer_namesstring[]NoNames of the manufacturers this product is linked to, up to 20 names of up to 200 characters. They are matched to your manufacturers ignoring case, and a name that does not exist creates a new manufacturer.
manufacturer_idintegerNoLinks every product created to this manufacturer. The ID comes from `manufacturers_list`.
{
  "products": [
    {
      "product_name": "Disposable syringe 5 ml, Luer lock",
      "nupco_product_code": "4111610529900",
      "catalogue_number": "SYR-5-LL",
      "cost_price": 0.12,
      "cost_currency": "USD",
      "sale_price_sar": 0.55,
      "is_medical": true
    },
    {
      "product_name": "Surgical gloves, sterile, size 7.5",
      "catalogue_number": "GLV-S-75",
      "manufacturer_names": [
        "Acme Medical Supplies"
      ]
    }
  ],
  "manufacturer_id": 88
}
Example arguments

Returns: A 201 reply with created (for each product its index in your list, id, product_name and nupco_product_code), created_count, and manufacturers_created, the names of manufacturers the call added to your directory. manufacturer_links_failed: true appears only when linking failed.

  • The products are saved first and the manufacturer links applied afterwards. If linking fails, the products stay and manufacturer_links_failed appears. Link them with `product_manufacturer_bulk_link` and do not repeat the call, which would create the products twice.
  • A batch that would pass your plan's product limit is refused whole with a 402, and an unknown manufacturer_id is refused with a 400 (manufacturer_not_found).
  • A row carries every field of the product form. Only keywords and the agency filter are set afterwards with `products_update`.
  • The server ignores the cost fields for the sales role.
  • A row stating price_basis without its count, or sending half of content_quantity and content_unit, refuses the whole batch with a 400 before anything is written, and rows names each refused row by its index, the field and the fix.

products_bulk_update

Changes up to 200 products in one call, all or nothing, the way the catalog grid saves. Every row must carry the grid's columns: the name, catalogue number, NUPCO code, cost, cost currency and sale price. An ID that does not match a live product of your organization rejects the whole batch.

Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
updatesobject[]YesThe product rows to change, from 1 to 200.
updates[].idintegerYesThe product's ID, from products_list.
updates[].product_namestringYesThe product's name, 1 to 200 characters. It replaces the current name.
updates[].catalogue_numberstringYesThe catalogue number, up to 100 characters. Send null to clear it.
updates[].nupco_product_codestringYesThe primary NUPCO code, up to 100 characters. Send null to clear it.
updates[].cost_pricenumberYesThe cost price in the currency of cost_currency, 0 to 9,999,999.99. Send null to clear it.
updates[].cost_currencystringYesThe cost price's currency: USD (US dollar) or SAR (Saudi riyal).
updates[].sale_price_sarnumberYesThe sale price in SAR, 0 to 9,999,999.99. Send null to clear it.
updates[].units_per_caseintegerNoUnits in one case, a whole number from 1 to 1,000,000. If you leave it out, the stored value is kept; null clears it.
updates[].is_medicalbooleanNoWhether the product is medical. If you leave it out, the stored value is kept.
{
  "updates": [
    {
      "id": 1207,
      "product_name": "Disposable syringe 5 ml, Luer lock",
      "catalogue_number": "SYR-5-LL",
      "nupco_product_code": "4111610529900",
      "cost_price": 0.12,
      "cost_currency": "USD",
      "sale_price_sar": 0.6,
      "units_per_case": 1000,
      "is_medical": true
    }
  ]
}
Example arguments

Returns: The number of products changed, in updated.

  • Each row replaces the fields it sends, and null clears one. units_per_case and is_medical keep their stored values when you leave them out. To change one field and nothing else, use `products_update`.
  • A field your role cannot see keeps its stored value whatever you send, so a sales call leaves each product's cost and currency as they are.
  • An unknown ID, or one in the trash, rejects the whole batch with a 400 (product_not_found).
  • A new NUPCO code is added to the product's links, and the old links stay.
  • The prices here are per each product's price_basis, which this tool does not change (change it with `products_update` and reprice). A row emptying a count its stated level needs, such as clearing units_per_case on a product priced per case, refuses the whole batch with a 400 (price_basis_level_missing) and rows naming each product.

The trash

Deleting takes two steps: a move to the trash, then a permanent delete. A product in the trash can be restored until it is permanently deleted.

products_delete

Moves a product to the trash. It leaves the lists, links and alerts and frees its slot in your plan, but nothing is erased, and `products_restore` brings it back.

Deletes or overwrites data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
idintegerYesThe product's ID, from products_list.

Returns: { "success": true }.

  • The tool answers 404 when the product does not exist or is already in the trash.

products_trash_list

Lists the products in the trash, most recently deleted first. Use it to find the ID of a product you want to restore or delete permanently.

Read-only · scope products.read · all roles · Pro and Enterprise plans

ParameterTypeRequiredDescription
pageintegerNoPage number, starting at 1.
page_sizeintegerNoResults per page: 20 by default, 100 at most.

Returns: A page of products in data, with total, page, page_size and total_pages, in the same shape as `products_list`.

  • Price fields are left out according to your role, as in the list.

products_restore

Moves a product from the trash back into the catalog. It counts against your plan's product limit again, so the tool answers with a 402 if the limit is reached.

Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
idintegerYesThe product's ID. It must be in the trash, and comes from `products_trash_list`.

Returns: The restored product, with photo_urls.

  • The tool answers 404 when the product is not in the trash.

products_purge

Permanently deletes one product from the trash, or everything in it, along with the photos and files. This cannot be undone. A product that was chosen for a tender item is never deleted, because the tender's history needs it.

Deletes or overwrites data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
idintegerNoThe ID of one product in the trash. Send id or all.
allbooleanNotrue deletes everything in the trash. If you send it together with id, all is what runs.
{
  "all": true
}
Example arguments

Returns: success and removed, the number deleted, plus blocked_ids when a product was kept because tender items use it. removed is 0 when nothing in the trash matched.

  • The tool answers 409 (referenced_by_tender_items, with blocked_ids) only when every target is blocked. If only some are blocked, the rest are deleted and the reply succeeds with blocked_ids, so always check it.
  • To free a blocked product, clear its selection on the tender items with `tender_item_product_selection_clear` and try again.
  • An ID that is not in the trash is no error and deletes nothing. A call with neither id nor all is refused with a 400.

A product can have one or more manufacturers from those registered in your organization. The manufacturers themselves are created and edited with the manufacturer tools.

Links a product to one of your organization's manufacturers. Linking a pair that is already linked changes nothing, and keeps whether that manufacturer is the product's default.

Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
product_idintegerYesThe product's ID, from products_list.
manufacturer_idintegerYesThe manufacturer's ID, from `manufacturers_list`.

Returns: The link: id, product_id, manufacturer_id, is_default and created_at.

  • A product that does not exist or is in the trash (product_not_found) and a manufacturer that is not in your organization (manufacturer_not_found) are refused with a 400.

Removes the link between a product and a manufacturer. Both records stay. If the manufacturer was the product's default, the product is left without a default.

Deletes or overwrites data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
product_idintegerYesThe product's ID, from products_list.
manufacturer_idintegerYesThe manufacturer's ID, from `manufacturers_list`.

Returns: { "success": true }.

  • Unlinking a pair that is not linked is no error and changes nothing.

Links up to 500 existing products to one manufacturer in a single call. Pairs that are already linked are left as they are.

Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
manufacturer_idintegerYesThe manufacturer's ID, from `manufacturers_list`.
product_idsinteger[]YesThe IDs of the products to link, from 1 to 500. A repeated ID counts once.
{
  "manufacturer_id": 88,
  "product_ids": [
    1207,
    1208,
    1215
  ]
}
Example arguments

Returns: { "linked": n }, the number of distinct products in the call, including any that were already linked.

  • A product ID that does not exist or is in the trash rejects the whole batch with a 400 (product_not_found), and an unknown manufacturer gives manufacturer_not_found.

product_manufacturer_set_default

Chooses which of a product's linked manufacturers is its default, or clears the choice. A product has at most one default, and choosing a new one replaces the old. Generated documents and quotation lines pick a manufacturer in this order: the one set on the line, then the default, then the only linked manufacturer.

Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
product_idintegerYesThe product's ID, from products_list.
manufacturer_idintegerYesThe manufacturer's ID. It must already be linked to the product, or the tool answers with a 400 (link_not_found).
is_defaultbooleanNotrue makes it the default and false clears the choice. Defaults to true.
{
  "product_id": 1207,
  "manufacturer_id": 88
}
Example arguments

Returns: The link after the change: id, product_id, manufacturer_id, is_default and created_at.

NUPCO codes

NUPCO codes are what tie a product to tender items. A product has a primary code in its nupco_product_code field and can be linked to further codes.

Links a product to a NUPCO material code. A product can carry several codes, and the linked codes are what match tender items to the product, drive its alerts and gather its price history. Linking the same pair again changes nothing.

Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
product_idintegerYesThe product's ID, from products_list.
nupco_codestringYesThe NUPCO material code, 1 to 100 characters. Spaces at the start and end are trimmed.
is_defaultbooleanNotrue makes this product the code's default, the one tenders_catalog_pricing prices an item with when several products share the code. A code has one default, so the previous one is cleared. false clears the mark, and leaving it out keeps it as it is.

Returns: The link: id, product_id, nupco_code, is_default and created_at.

  • The nupco_product_code field links itself when the product is saved, so there is no need to link the primary code with this tool.
  • A product that does not exist or is in the trash is refused with a 400 (product_not_found). The same code may be linked to several products.

Removes one link between a product and a NUPCO code. It removes the link only: the product's own nupco_product_code field stays as it is, and the product still matches by that code if it is the primary one.

Deletes or overwrites data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
product_idintegerYesThe product's ID, from products_list.
nupco_codestringYesThe NUPCO material code, 1 to 100 characters. Spaces at the start and end are trimmed.

Returns: { "success": true }.

  • To stop matching by the primary code, clear it from the field with `products_update` (nupco_product_code: null) and then unlink it here.
  • The code is matched exactly after trimming, and unlinking a pair that is not linked is no error.

Price history and tender alerts

Three read tools: the prices suppliers have quoted your organization, what NUPCO tenders were awarded at, and the open tenders that match your catalog. All three belong to the Pro and Enterprise plans.

products_sourcing_history

Lists every supplier price your organization has recorded for this product across all tenders and standalone RFQs, newest first. A price counts for the product when it was entered on the product itself, on a tender item you chose this product for, or on an item carrying one of the product's linked NUPCO codes.

Read-only · scope products.read · roles: owner, admin, purchasing, viewer · Pro and Enterprise plans

ParameterTypeRequiredDescription
idintegerYesThe product's ID, from products_list.

Returns: { "quotes": [...] }. Each entry has unitPriceUsd, unitPriceSar, quotedUnitPrice in the supplier's currency, fxRate, vendorName, leadTimeDays, submittedAt and tenderLabel, plus matchBasis, which is product or code and says how the price was tied to the product.

  • At most 50 entries come back, and lines with no price are left out.
  • These are cost-side prices, so the sales role cannot reach them. The tool answers 404 when the product does not exist or is in the trash.

products_historical_prices

Lists the published NUPCO award results for this product: every award row whose material code matches the product's own code or one of its linked codes, from all vendors. Rows your organization won are flagged with awarded_to_us.

Read-only · scope products.read · roles: owner, admin, sales, purchasing, viewer · Pro and Enterprise plans

ParameterTypeRequiredDescription
idintegerYesThe product's ID, from products_list.

Returns: { "prices": [...], "prices_hidden": null }. Each row has unit_price, currency, uom, vendor_name, manufacturer, country, catalog_no, source_tender_number, award_date, awarded_to_us and price_hidden, plus price_hidden_reason on a row hidden until the subscription is paid. prices_hidden is unpaid when prices are hidden for that reason.

  • Rows run from the newest award period to the oldest, cheapest first within a period. At most 50 rows come back.
  • unit_price is null when the award announcement named the winner but published no price. null means not published, never zero, and those rows come after the priced ones.
  • Until your organization's first subscription payment clears, rows may come back without prices: a row hidden for that reason has unit_price null, price_hidden set to true and price_hidden_reason set to unpaid, and rows within a period are ordered by vendor. The price exists and is hidden; it is not an unpublished one.
  • A row with price_hidden set to true and no price_hidden_reason has a price your organization does not see, ranked with the rows that have no price. The price exists and is hidden; it is not an unpublished one.
  • Each read counts against your organization's per-minute budget for searches of tenders and price history, shared by every key, connector and the web app. When it runs out the tool answers 429 rate_limited with the seconds to wait.
  • A product with no NUPCO code returns an empty list, and the tool answers 404 when the product does not exist.

products_alerts_list

Lists the open tenders that match your catalog, grouped by tender with the products that matched it. No tool creates a product alert: a product starts matching when it has a search query (search_query on products_update) or a linked NUPCO code (product_nupco_link), and its matches then appear here and in notifications.

Read-only · scope products.read · all roles · Pro and Enterprise plans

No parameters.

Returns: tenders: a list of { tender, products }, the nearest deadline first and at most 200 tenders. Each entry in products has product (id and product_name) and via_item, which is true when the match came through an item of the tender rather than its own text. products_total is the number of products in your catalog.

  • A product matches tenders through its query or its linked NUPCO codes, never by its name or its keywords. agency_filter narrows the matches by text in the agency's name.
  • The list is prepared ahead and refreshed after each tender scrape, and a change to a product shows in the next call.

The compliance letter

One letter, generated on request and never stored.

product_compliance_letter_pdf

Generates a PDF of the compliance letter that stands in for the product's MDMA certificate in a tender submission, printed on your organization's letterhead with its stamp. Nothing is stored: each call renders a new letter. When you leave out letter_type the tool chooses: under_registration when the MDMA status is under registration or the product is medical, otherwise non_medical_attestation.

Read-only · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans

ParameterTypeRequiredDescription
idintegerYesThe product's ID, from products_list.
letter_typestringNoThe letter type: under_registration (a registration status update) or non_medical_attestation (an attestation that the product is non-medical). It overrides the automatic choice.
recipientstringNoWho the letter is addressed to, 1 to 300 characters. Defaults to the National Unified Procurement Company (NUPCO) by its English name. The salutation uses the part of the text before the first dash.
notesstringNoAn extra sentence added to the body of the letter, up to 1,000 characters. Leave it out or send null for no addition.
{
  "id": 1207,
  "letter_type": "under_registration",
  "notes": "The MDMA application was filed in March 2026."
}
Example arguments

Returns: A PDF file with filename, content_type, size_bytes and base64, as described in Files and background jobs.

  • The under_registration letter has two pages, English then Arabic, and cites the MDMA number as the registration reference when the product has one. The non_medical_attestation letter is one English page attesting regulatory compliance outside the medical device regime.
  • The letter prints your organization's English name, logo, stamp and registration details from the organization settings. The tool needs the products.write scope and a role that edits products, although it changes nothing.
  • It works for a product in the trash too, and answers 404 when the product does not exist. A file over 4 MB is not returned in the reply.