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.
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.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.
| Tool | What it does |
|---|---|
| products_list | List products |
| products_get | Get a product |
| products_similar | Products with the same NUPCO code |
| catalog_files_list | Search the document library |
| products_export_csv | Export the whole catalog |
| products_import_template | Download the products import template |
| products_create | Create a product |
| products_update | Update a product |
| products_bulk_create | Create many products |
| products_bulk_update | Update many products |
| products_delete | Move a product to the trash |
| products_trash_list | List trashed products |
| products_restore | Restore a product from the trash |
| products_purge | Delete products permanently |
| product_manufacturer_link | Link a product to a manufacturer |
| product_manufacturer_unlink | Unlink a product from a manufacturer |
| product_manufacturer_bulk_link | Link many products to a manufacturer |
| product_manufacturer_set_default | Set the default manufacturer |
| product_nupco_link | Link a product to a NUPCO code |
| product_nupco_unlink | Unlink a product from a NUPCO code |
| products_sourcing_history | Product supplier price history |
| products_historical_prices | Product NUPCO award history |
| products_alerts_list | List product alerts |
| product_compliance_letter_pdf | Generate 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
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number, starting at 1. |
page_size | integer | No | Results per page: 20 by default, 100 at most. |
q | string | No | The 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_id | integer | No | Only products linked to this manufacturer. The ID comes from `manufacturers_list`. When no product is linked to it, the page is empty. |
include_manufacturers | boolean | No | true adds a manufacturers field to each product: the list of its manufacturers, each with id, name and is_default. |
group_by | string | No | nupco_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
}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_bythe reply hasgroupsinstead ofdata. Each group hasnupco_code(nullfor the products with no code),product_count,offsetandproducts, and each product in it carriesnupco_codes, all its codes. The reply addstotal(the products matched),total_codes,products_without_codeandtotal_rows. Pages count rows, not groups, so a group can continue on the next page, where itsoffsetis 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The product's ID, from products_list. |
{
"id": 42
}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
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | No | The 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. |
scope | string | No | all (the default), products for product files only, manufacturers for manufacturer files only, or sales_orders for sales order documents only. |
category | string | No | One 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_id | integer | No | Only this manufacturer's files and the files of the products linked to it. The ID comes from `manufacturers_list`. |
page | integer | No | Page number, starting at 1. |
page_size | integer | No | Files per page: 50 by default, 100 at most. |
{
"q": "syringe cert",
"scope": "products",
"page_size": 20
}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.readscope, and sales order documents only when it hasorders.readand 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_getor `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
| Parameter | Type | Required | Description |
|---|---|---|---|
locale | string | No | The language of the instructions sheet and the header notes: ar or en, en by default. The headers stay English in both. |
{
"locale": "ar"
}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
productstarget 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
| Parameter | Type | Required | Description |
|---|---|---|---|
product_name | string | Yes | The product's name, 1 to 200 characters. |
description | string | No | A free-form description, up to 2,000 characters. |
keywords | string[] | No | Keywords 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_code | string | No | The product's primary NUPCO material code, up to 100 characters. It is also added to the product's NUPCO code links. |
search_query | string | No | A 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_filter | string | No | Text the buying agency's name must contain for a match to count, up to 200 characters. |
weight_kg | number | No | Unit weight in kg, 0 to 99,999.999. |
length_cm | number | No | Unit length in cm, 0 to 99,999.99. |
width_cm | number | No | Unit width in cm, 0 to 99,999.99. |
height_cm | number | No | Unit height in cm, 0 to 99,999.99. |
units_per_case | integer | No | Units 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_type | string | No | The 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_kg | number | No | Case weight in kg, 0 to 99,999.999. |
case_length_cm | number | No | Case length in cm, 0 to 99,999.99. |
case_width_cm | number | No | Case width in cm, 0 to 99,999.99. |
case_height_cm | number | No | Case height in cm, 0 to 99,999.99. |
units_per_inner | integer | No | An alternative name for package_size (units in one pack), whole numbers only. The system keeps the two equal, so prefer package_size. |
inner_length_cm | number | No | Length in cm of the pack that package_size describes, 0 to 99,999.99. |
inner_width_cm | number | No | Pack width in cm, 0 to 99,999.99. |
inner_height_cm | number | No | Pack height in cm, 0 to 99,999.99. |
unit_of_measure | string | No | The name of one unit, the smallest level of the unit, pack and case hierarchy, for example Piece. Up to 50 characters. |
country_of_origin_code | string | No | The country of origin as a two-letter uppercase ISO 3166-1 code, such as SA or DE. |
catalogue_number | string | No | The manufacturer's catalogue number, up to 100 characters. |
mdma_status | string | No | The 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_number | string | No | The SFDA listing number, up to 200 characters. |
min_order_qty | integer | No | The smallest quantity the supplier accepts per order, a whole number from 1 to 1,000,000. |
mdma_number | string | No | The 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_expiry | string | No | The expiry date of the MDMA authorization, as YYYY-MM-DD. |
shelf_life_years | number | No | The product's shelf life in years, fractions allowed, above 0 and up to 9,999.99. |
first_delivery_days | integer | No | Lead time for the first delivery, in days, 0 to 3,650. When it is empty the product inherits your organization's default. |
order_delivery_days | integer | No | Lead time for later orders, in days, 0 to 3,650. When it is empty the product inherits your organization's default. |
volume | number | No | The volume of the sellable unit, in the unit named by volume_unit, for example 500. |
volume_unit | string | No | The unit of the volume, such as ml or L. Up to 50 characters. |
content_quantity | number | No | What 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_unit | string | No | The 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_type | string | No | The 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_size | number | No | Units 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_uom | string | No | An alternative name for unit_of_measure (the unit's name). The system keeps the two equal, so prefer unit_of_measure. |
packing_kind | string | No | The 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_percent | number | No | The 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_percent | number | No | This 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_info | string | No | Free text for the additional information line of the product's info file, up to 2,000 characters. |
price_basis | string | No | The 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_price | number | No | The cost price in the currency of cost_currency, 0 to 9,999,999.99, for one price_basis level. |
cost_currency | string | No | The currency of cost_price: USD (US dollar) or SAR (Saudi riyal). Defaults to USD. |
sale_price_sar | number | No | The sale price in SAR, 0 to 9,999,999.99, for one price_basis level. |
is_medical | boolean | No | Whether 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
}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,limitandcurrent. - Fields your role cannot see are ignored on write, so sales does not save
cost_priceorcost_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_basiswith the prices, and the level's count with it:price_basis: "case"withunits_per_case. A level missing its count is refused with a 400 (price_basis_level_missing) that names the field, andcontent_quantitywithoutcontent_unitwith a 400 (content_incomplete). - To import a list from an Excel or CSV file, use `sheet_import` with the
productstarget.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The product's ID, from products_list. |
product_name | string | No | The product's new name, 1 to 200 characters. |
description | string | No | A free-form description, up to 2,000 characters. Send null to clear it. |
keywords | string[] | No | Keywords 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_code | string | No | The 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_query | string | No | A 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_filter | string | No | Text the buying agency's name must contain for a match to count, up to 200 characters. Send null to clear it. |
weight_kg | number | No | Unit weight in kg, 0 to 99,999.999. Send null to clear it. |
length_cm | number | No | Unit length in cm, 0 to 99,999.99. Send null to clear it. |
width_cm | number | No | Unit width in cm, 0 to 99,999.99. Send null to clear it. |
height_cm | number | No | Unit height in cm, 0 to 99,999.99. Send null to clear it. |
units_per_case | integer | No | Units 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_type | string | No | The 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_kg | number | No | Case weight in kg, 0 to 99,999.999. Send null to clear it. |
case_length_cm | number | No | Case length in cm, 0 to 99,999.99. Send null to clear it. |
case_width_cm | number | No | Case width in cm, 0 to 99,999.99. Send null to clear it. |
case_height_cm | number | No | Case height in cm, 0 to 99,999.99. Send null to clear it. |
units_per_inner | integer | No | An 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_cm | number | No | Length in cm of the pack that package_size describes, 0 to 99,999.99. Send null to clear it. |
inner_width_cm | number | No | Pack width in cm, 0 to 99,999.99. Send null to clear it. |
inner_height_cm | number | No | Pack height in cm, 0 to 99,999.99. Send null to clear it. |
unit_of_measure | string | No | The 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_code | string | No | The country of origin as a two-letter uppercase ISO 3166-1 code, such as SA or DE. Send null to clear it. |
catalogue_number | string | No | The manufacturer's catalogue number, up to 100 characters. Send null to clear it. |
mdma_status | string | No | The 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_number | string | No | The SFDA listing number, up to 200 characters. Send null to clear it. |
min_order_qty | integer | No | The smallest quantity the supplier accepts per order, a whole number from 1 to 1,000,000. Send null to clear it. |
mdma_number | string | No | The 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_expiry | string | No | The expiry date of the MDMA authorization, as YYYY-MM-DD. Send null to clear it. |
shelf_life_years | number | No | The product's shelf life in years, fractions allowed, above 0 and up to 9,999.99. Send null to clear it. |
first_delivery_days | integer | No | Lead 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_days | integer | No | Lead 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. |
volume | number | No | The volume of the sellable unit, in the unit named by volume_unit, for example 500. Send null to clear it. |
volume_unit | string | No | The unit of the volume, such as ml or L. Up to 50 characters. Send null to clear it. |
content_quantity | number | No | What 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_unit | string | No | The 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_type | string | No | The 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_size | number | No | Units 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_uom | string | No | An 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_kind | string | No | The 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_percent | number | No | The 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_percent | number | No | This 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_info | string | No | Free text for the additional information line of the product's info file, up to 2,000 characters. Send null to clear it. |
price_basis | string | No | The 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_price | number | No | The 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_currency | string | No | The currency of cost_price: USD (US dollar) or SAR (Saudi riyal). Defaults to USD. |
sale_price_sar | number | No | The sale price in SAR, 0 to 9,999,999.99, for one price_basis level. Send null to clear it. |
is_medical | boolean | No | Whether the product is a medical device or supply. Defaults to false. It decides the compliance letter's type when the MDMA status does not. |
reprice | string | No | What 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
}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_codeadds a link and does not remove the old one, and sendingnullclears the field and leaves the links as they are. - Marking a product medical here does not change
vat_percent: set it yourself. - Stating
price_basison 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 changeprice_basisor sendreprice: 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_missingwhen the stated level's count is empty (setpackage_size,units_per_caseorcontent_quantityin the same call, or choose another level),content_incompletewhen one ofcontent_quantityandcontent_unitcomes without the other,reprice_requiredwhen a stated level changes or is cleared on a product with a saved price you do not resend (sendrepriceor the new prices), andconverted_price_out_of_rangewhen a converted price would pass 9,999,999.99 (send the prices yourself orreprice: "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
| Parameter | Type | Required | Description |
|---|---|---|---|
products | object[] | Yes | The product rows to create, from 1 to 500. |
products[].product_name | string | Yes | The product's name, 1 to 200 characters. |
products[].description | string | No | A free-form description, up to 2,000 characters. |
products[].nupco_product_code | string | No | The product's primary NUPCO material code, up to 100 characters. It is also added to the product's NUPCO code links. |
products[].catalogue_number | string | No | The manufacturer's catalogue number, up to 100 characters. |
products[].cost_price | number | No | The cost price in the currency of cost_currency, 0 to 9,999,999.99, for one price_basis level. |
products[].cost_currency | string | No | The currency of cost_price: USD (US dollar) or SAR (Saudi riyal). Defaults to USD. |
products[].sale_price_sar | number | No | The sale price in SAR, 0 to 9,999,999.99, for one price_basis level. |
products[].units_per_case | integer | No | Units 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_medical | boolean | No | Whether 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_basis | string | No | The 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_type | string | No | The 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_quantity | number | No | What 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_unit | string | No | The 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_kind | string | No | The 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_query | string | No | A 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_measure | string | No | The 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_code | string | No | The country of origin as a two-letter uppercase ISO 3166-1 code, such as SA or DE. |
products[].mdma_status | string | No | The 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_number | string | No | The SFDA listing number, up to 200 characters. |
products[].vat_percent | number | No | This 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_kg | number | No | Unit weight in kg, 0 to 99,999.999. |
products[].length_cm | number | No | Unit length in cm, 0 to 99,999.99. |
products[].width_cm | number | No | Unit width in cm, 0 to 99,999.99. |
products[].height_cm | number | No | Unit height in cm, 0 to 99,999.99. |
products[].inner_length_cm | number | No | Length in cm of the pack that package_size describes, 0 to 99,999.99. |
products[].inner_width_cm | number | No | Pack width in cm, 0 to 99,999.99. |
products[].inner_height_cm | number | No | Pack height in cm, 0 to 99,999.99. |
products[].case_weight_kg | number | No | Case weight in kg, 0 to 99,999.999. |
products[].case_length_cm | number | No | Case length in cm, 0 to 99,999.99. |
products[].case_width_cm | number | No | Case width in cm, 0 to 99,999.99. |
products[].case_height_cm | number | No | Case height in cm, 0 to 99,999.99. |
products[].min_order_qty | integer | No | The smallest quantity the supplier accepts per order, a whole number from 1 to 1,000,000. |
products[].mdma_number | string | No | The 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_expiry | string | No | The expiry date of the MDMA authorization, as YYYY-MM-DD. |
products[].shelf_life_years | number | No | The product's shelf life in years, fractions allowed, above 0 and up to 9,999.99. |
products[].first_delivery_days | integer | No | Lead 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_days | integer | No | Lead time for later orders, in days, 0 to 3,650. When it is empty the product inherits your organization's default. |
products[].volume | number | No | The volume of the sellable unit, in the unit named by volume_unit, for example 500. |
products[].volume_unit | string | No | The unit of the volume, such as ml or L. Up to 50 characters. |
products[].package_type | string | No | The 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_size | number | No | Units 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_uom | string | No | An alternative name for unit_of_measure (the unit's name). The system keeps the two equal, so prefer unit_of_measure. |
products[].foc_percent | number | No | The 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_info | string | No | Free text for the additional information line of the product's info file, up to 2,000 characters. |
products[].manufacturer_names | string[] | No | Names 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_id | integer | No | Links 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
}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_failedappears. 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_idis 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_basiswithout its count, or sending half ofcontent_quantityandcontent_unit, refuses the whole batch with a 400 before anything is written, androwsnames each refused row by itsindex, 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
| Parameter | Type | Required | Description |
|---|---|---|---|
updates | object[] | Yes | The product rows to change, from 1 to 200. |
updates[].id | integer | Yes | The product's ID, from products_list. |
updates[].product_name | string | Yes | The product's name, 1 to 200 characters. It replaces the current name. |
updates[].catalogue_number | string | Yes | The catalogue number, up to 100 characters. Send null to clear it. |
updates[].nupco_product_code | string | Yes | The primary NUPCO code, up to 100 characters. Send null to clear it. |
updates[].cost_price | number | Yes | The cost price in the currency of cost_currency, 0 to 9,999,999.99. Send null to clear it. |
updates[].cost_currency | string | Yes | The cost price's currency: USD (US dollar) or SAR (Saudi riyal). |
updates[].sale_price_sar | number | Yes | The sale price in SAR, 0 to 9,999,999.99. Send null to clear it. |
updates[].units_per_case | integer | No | Units 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_medical | boolean | No | Whether 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
}
]
}Returns: The number of products changed, in updated.
- Each row replaces the fields it sends, and
nullclears one.units_per_caseandis_medicalkeep 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` andreprice). A row emptying a count its stated level needs, such as clearingunits_per_caseon a product priced per case, refuses the whole batch with a 400 (price_basis_level_missing) androwsnaming 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number, starting at 1. |
page_size | integer | No | Results 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | No | The ID of one product in the trash. Send id or all. |
all | boolean | No | true deletes everything in the trash. If you send it together with id, all is what runs. |
{
"all": true
}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, withblocked_ids) only when every target is blocked. If only some are blocked, the rest are deleted and the reply succeeds withblocked_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
idnorallis refused with a 400.
Manufacturer links
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.
product_manufacturer_link
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
| Parameter | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | The product's ID, from products_list. |
manufacturer_id | integer | Yes | The 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.
product_manufacturer_unlink
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
| Parameter | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | The product's ID, from products_list. |
manufacturer_id | integer | Yes | The manufacturer's ID, from `manufacturers_list`. |
Returns: { "success": true }.
- Unlinking a pair that is not linked is no error and changes nothing.
product_manufacturer_bulk_link
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
| Parameter | Type | Required | Description |
|---|---|---|---|
manufacturer_id | integer | Yes | The manufacturer's ID, from `manufacturers_list`. |
product_ids | integer[] | Yes | The IDs of the products to link, from 1 to 500. A repeated ID counts once. |
{
"manufacturer_id": 88,
"product_ids": [
1207,
1208,
1215
]
}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 givesmanufacturer_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
| Parameter | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | The product's ID, from products_list. |
manufacturer_id | integer | Yes | The manufacturer's ID. It must already be linked to the product, or the tool answers with a 400 (link_not_found). |
is_default | boolean | No | true makes it the default and false clears the choice. Defaults to true. |
{
"product_id": 1207,
"manufacturer_id": 88
}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.
product_nupco_link
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
| Parameter | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | The product's ID, from products_list. |
nupco_code | string | Yes | The NUPCO material code, 1 to 100 characters. Spaces at the start and end are trimmed. |
is_default | boolean | No | true 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_codefield 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.
product_nupco_unlink
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
| Parameter | Type | Required | Description |
|---|---|---|---|
product_id | integer | Yes | The product's ID, from products_list. |
nupco_code | string | Yes | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The 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_priceisnullwhen the award announcement named the winner but published no price.nullmeans 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_pricenull,price_hiddenset totrueandprice_hidden_reasonset tounpaid, 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_hiddenset totrueand noprice_hidden_reasonhas 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_limitedwith 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_filternarrows 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
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The product's ID, from products_list. |
letter_type | string | No | The 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. |
recipient | string | No | Who 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. |
notes | string | No | An 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."
}Returns: A PDF file with filename, content_type, size_bytes and base64, as described in Files and background jobs.
- The
under_registrationletter has two pages, English then Arabic, and cites the MDMA number as the registration reference when the product has one. Thenon_medical_attestationletter 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.writescope 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.