MCP tools
Photos, attachments and spreadsheet imports
MCP tools for product photos and attachments, manufacturer attachments, your organization's logo, stamp and signature, and importing records from a spreadsheet.
This page gathers the tools that handle files: product photos and attachments, manufacturer attachments, your organization's logo and stamp, and your own signature. It also holds sheet_import, the tool that turns a spreadsheet into records.
A file never travels inside a tool call. Every upload is two steps: the start tool, such as product_photo_upload_url_create, checks the record and the declared file and returns a temporary upload link, upload_url, to which you send the file's bytes directly with a PUT, from a browser or any program. Then the matching finalize tool, such as product_photo_upload_finalize, checks the file's real size and type, processes an image where needed, and files it on its record. So no request size limits the file, and its bytes never pass through the agent's context.
const blob = await (await fetch(SOURCE_URL)).blob();
await fetch(upload_url, { method: "PUT", headers: { "Content-Type": blob.type }, body: blob });A file the platform already holds, such as one sent to the WhatsApp assistant, is not uploaded at all: send its reference in the staged_ref field to the start tool, which copies it into place inside the server, then call the finalize tool straight away. That is how a full catalog of up to 100 MB gets filed.
| Files | Accepted formats | Largest size |
|---|---|---|
| Product photos | JPEG, PNG, WebP, GIF; AVIF and TIFF are converted to JPEG or PNG | 5 MB |
| Product and manufacturer attachments | PDF, images (JPEG, PNG, WebP, GIF), Word documents, Excel workbooks, PowerPoint decks, plain text, CSV | 15 MB uploaded, up to 100 MB with staged_ref |
| Logo, stamp and signature | PNG, JPEG, WebP, SVG; GIF, AVIF and TIFF are converted to JPEG or PNG | 2 MB |
sfda_cert and catalog on a product is what the product document on quotations and the NUPCO attachments file draw from, and a manufacturer's authorization letter rides with its products in submission packs. File each document once instead of attaching it to every quotation.| Tool | What it does |
|---|---|
| product_photo_list | List product photos |
| product_photo_upload_url_create | Start a product photo upload |
| product_photo_upload_finalize | Finish a product photo upload |
| product_photo_delete | Delete a product photo |
| product_attachment_upload_url_create | Start a product attachment upload |
| product_attachment_upload_finalize | Finish a product attachment upload |
| product_attachment_delete | Delete a product attachment |
| product_files_copy | Copy a product file to other products |
| manufacturer_attachment_list | List manufacturer attachments |
| manufacturer_attachment_upload_url_create | Start a manufacturer attachment upload |
| manufacturer_attachment_upload_finalize | Finish a manufacturer attachment upload |
| manufacturer_attachment_delete | Delete a manufacturer attachment |
| sales_order_document_upload_url_create | Start a sales order document upload |
| sales_order_document_upload_finalize | Finish a sales order document upload |
| sales_order_document_delete | Delete a sales order document |
| settings_logo_upload_url_create | Start an organization logo upload |
| settings_logo_upload_finalize | Finish an organization logo upload |
| settings_logo_delete | Delete the organization logo |
| settings_stamp_upload_url_create | Start an organization stamp upload |
| settings_stamp_upload_finalize | Finish an organization stamp upload |
| settings_stamp_delete | Delete the organization stamp |
| settings_signature_get | Get my signature |
| settings_signature_upload_url_create | Start a signature upload |
| settings_signature_upload_finalize | Finish a signature upload |
| settings_signature_delete | Delete my signature |
| sheet_import | Create records from a spreadsheet |
Product photos
A product's photos show in the product list and on the printed quotation lines linked to it. In the app, see Photos and files.
product_photo_list
Lists a product's photos, each with a path and a temporary link to view it. Use it to find the path that product_photo_delete needs, since `products_get` returns the links alone.
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: A list of {path, url} pairs, one per photo; each url is a link valid for about an hour. A product without photos gives an empty list.
- A product that your organization does not own, or that sits in the trash, answers 404.
product_photo_upload_url_create
Starts adding a photo to a catalog product: checks the product and the declared file, then returns a temporary upload link to send the photo to directly with a PUT. After the upload, call product_photo_upload_finalize. Every photo is added without replacing the others.
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. |
filename | string | No | The original file name. Required unless you send staged_ref, in which case the held file's own name is used unless you rename it. |
content_type | string | No | The file's MIME type: image/jpeg, image/png, image/webp or image/gif; image/avif and image/tiff are accepted and converted on finalize. Required unless you send staged_ref. |
size_bytes | integer | No | The file's size in bytes, if known, so a file over the limit is refused here instead of after the upload. |
staged_ref | string | No | A reference to a file the platform already holds for your organization, such as one sent to the WhatsApp assistant, listed by `whatsapp_attachments_list`. The file is copied into place inside the server, so there is nothing to upload: upload_url comes back empty and you go straight to the finalize tool. Send it instead of filename and content_type. |
{
"id": 1187,
"filename": "syringe-10ml-front.jpg",
"content_type": "image/jpeg",
"size_bytes": 482113
}Returns: upload_id, upload_url, method (PUT), headers and expires_at (status 201). With staged_ref, upload_url is empty and staged is true.
- The limit is 5 MB. A product that your organization does not own, or that sits in the trash, answers 404.
- The link takes one upload and expires at
expires_at, two hours after it was created. Nothing is filed before the finalize tool runs, and an upload not finalized within 24 hours is deleted along with its file. - An unsupported type answers 400 and a declared size over the limit answers 413; neither creates a link.
- Each organization, every API key and connector together, may hold at most 20 uploads open at once (started, not yet finalized, link unexpired) and start at most 500 a day (UTC), unless ProcDuck has set the organization other limits. Past either, the start tool answers 429 with
code: "upload_limit_reached"andretry_after_seconds: finalize the uploads whose file you sent, or wait that long. A link never used gives its place back when it expires.
product_photo_upload_finalize
Finishes an upload started with product_photo_upload_url_create: checks the photo's real size and type, converts, trims or downscales it where needed, then adds it to the product.
Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
upload_id | string | Yes | The upload_id the matching start tool returned. |
trim_whitespace | boolean | No | Crops near-white or transparent borders around the image, leaving padding of about 2% of the content's size. An SVG is left as it is. |
max_width | integer | No | Downscales the image to at most this many pixels wide, keeping its proportions; it never enlarges one. From 16 to 10000. |
{
"upload_id": "5b0f3c1e-8a4d-4f6b-9e2a-7c1d3e5f9a02",
"trim_whitespace": true,
"max_width": 1600
}Returns: The new photo's path and url (a link valid for about an hour), with content_type, size_bytes, width, height, converted_from when the format was converted, trimmed and resized. Keep path to delete or copy the photo later.
- An image in a format outside the allowed ones, such as AVIF or TIFF, is converted to JPEG at quality 85, or to PNG when it has transparency. An image that needs no conversion, trim or downscale is stored exactly as uploaded. HEIC is not accepted; export it as JPEG or PNG.
- The server reads the file itself rather than trusting the declared type: a file that is not an accepted type, or is over the limit, is deleted and answers 422 or 413 with the reason, so start a new upload.
- Each
upload_idfinalizes once. An unknown one, or one from another organization, answers 404; an expired one answers 410; one already used answers 409. - Calling finalize before the upload answers 409 without spending it: upload the file to
upload_url, then call it again.
product_photo_delete
Permanently deletes one photo of a product. This cannot be undone.
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. |
path | string | Yes | The photo's path, from product_photo_list or from the reply of product_photo_upload_finalize. |
Returns: A confirmation (success: true).
- A
paththat is not one of this product's photos answers 400, and a product your organization does not own answers 404.
Product attachments
A product's documents are filed in three categories: sfda_cert, catalog and other. The list of attachments with their paths comes from `products_get`, in its attachments array.
product_attachment_upload_url_create
Starts filing a document on a catalog product: its SFDA certificate, its catalog or another file. Returns a temporary upload link to send the file to with a PUT, or copies a file the platform already holds when you send staged_ref. Then call product_attachment_upload_finalize.
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. |
category | string | Yes | The attachment's category: sfda_cert for the SFDA certificate, catalog for the catalog or datasheet, other for any other file. |
filename | string | No | The original file name. Required unless you send staged_ref, in which case the held file's own name is used unless you rename it. |
content_type | string | No | The file's MIME type; required unless you send staged_ref. Accepted: application/pdf, images (image/jpeg, image/png, image/webp, image/gif), Office documents (Word, Excel, PowerPoint, old and OpenXML formats), text/plain and text/csv. |
size_bytes | integer | No | The file's size in bytes, if known, so a file over the limit is refused here instead of after the upload. |
staged_ref | string | No | A reference to a file the platform already holds for your organization, such as one sent to the WhatsApp assistant, listed by `whatsapp_attachments_list`. The file is copied into place inside the server, so there is nothing to upload: upload_url comes back empty and you go straight to the finalize tool. Send it instead of filename and content_type. |
{
"id": 1187,
"category": "catalog",
"filename": "syringe-catalog-2026.pdf",
"content_type": "application/pdf",
"size_bytes": 2864117
}Returns: upload_id, upload_url, method (PUT), headers and expires_at (status 201). With staged_ref, upload_url is empty and staged is true.
- The limit is 15 MB for an uploaded file and 100 MB for a referenced one. A product that your organization does not own, or that sits in the trash, answers 404.
- The link takes one upload and expires at
expires_at, two hours after it was created. Nothing is filed before the finalize tool runs, and an upload not finalized within 24 hours is deleted along with its file. - An unsupported type answers 400 and a declared size over the limit answers 413; neither creates a link.
- Each organization, every API key and connector together, may hold at most 20 uploads open at once (started, not yet finalized, link unexpired) and start at most 500 a day (UTC), unless ProcDuck has set the organization other limits. Past either, the start tool answers 429 with
code: "upload_limit_reached"andretry_after_seconds: finalize the uploads whose file you sent, or wait that long. A link never used gives its place back when it expires.
product_attachment_upload_finalize
Finishes an upload started with product_attachment_upload_url_create: checks the file's real size and type, then files it on the product under the category chosen at the start.
Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
upload_id | string | Yes | The upload_id the matching start tool returned. |
{
"upload_id": "0e7b2d94-3c51-4a8f-b6d2-9f4e1a7c3b58"
}Returns: The new attachment's path, url (a link valid for about an hour) and name, with content_type and size_bytes. The file is stored under the type its contents prove.
- A
staged_refis used once: after a successful finalize the held copy is discarded, and if the file is refused the reference becomes usable again. - The server reads the file itself rather than trusting the declared type: a file that is not an accepted type, or is over the limit, is deleted and answers 422 or 413 with the reason, so start a new upload.
- Each
upload_idfinalizes once. An unknown one, or one from another organization, answers 404; an expired one answers 410; one already used answers 409. - Calling finalize before the upload answers 409 without spending it: upload the file to
upload_url, then call it again.
product_attachment_delete
Permanently deletes one attachment of a product. This cannot be undone.
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. |
category | string | Yes | The attachment's category, one of the values used when uploading: sfda_cert, catalog or other. |
path | string | Yes | The attachment's path, from the attachments array of products_get or from the upload reply. |
{
"id": 1187,
"category": "catalog",
"path": "9b2f1c0e-4d1a-4f6e-8c1b-3a7d5e2f9a10/products/1187/attachments/catalog/1791281700000-k3x9ab__syringe-catalog-2026.pdf"
}Returns: A confirmation (success: true).
- A
paththat is not among this product's attachments of the givencategoryanswers 400, and a product your organization does not own answers 404.
product_files_copy
Puts one product's photo or attachment on other products of your organization by a copy inside storage, at the same cost whatever the file's size. Upload a brochure that covers several products once, then copy it here instead of uploading it for each. Each product gets its own copy, so deleting it from one leaves the others theirs.
Changes data · scope products.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | photo for a photo, or the attachment category the source file is filed under and the copies will be: sfda_cert, catalog or other. |
source_product_id | integer | Yes | The product the file belongs to now. |
path | string | Yes | The source file's path: a photo's from `product_photo_list` or its upload reply, an attachment's from the attachments of products_get or its upload reply. |
product_ids | integer[] | Yes | The products to put the file on, from 1 to 500. The source product is skipped if listed. |
{
"kind": "catalog",
"source_product_id": 1187,
"path": "9b2f1c0e-4d1a-4f6e-8c1b-3a7d5e2f9a10/products/1187/attachments/catalog/1791281700000-k3x9ab__syringe-catalog-2026.pdf",
"product_ids": [
1188,
1189,
1190
]
}Returns: An object with copied (each product's product_id and the new path) and failed (each product's product_id and the error).
- A
paththat is not in the source product's folder for that kind answers 400, and any product your organization does not own or that is in the trash answers 404 with nothing copied. One copy failing does not stop the rest: retry the products listed infailedalone.
Manufacturer attachments
A manufacturer has four categories of attachments: catalog, sfda_ar, iso and authorization. Attachments and files explains how the app uses them.
manufacturer_attachment_list
Lists every file filed on a manufacturer across its four categories, each with its path, display name, size and a temporary download link.
Read-only · scope manufacturers.read · all roles · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The manufacturer's ID, from manufacturers_list. |
Returns: A list of attachments, each with category, path, name, size, uploaded_at and url, a link valid for about an hour.
- A manufacturer that your organization does not own answers 404.
manufacturer_attachment_upload_url_create
Starts filing a document on a manufacturer: its catalog, its SFDA authorized-representative licence, an ISO certificate or the distributor authorization letter your organization holds from it. Returns a temporary upload link, or copies a file the platform holds when you send staged_ref, which is how a full supplier catalog gets filed. Then call manufacturer_attachment_upload_finalize.
Changes data · scope manufacturers.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The manufacturer's ID, from manufacturers_list. |
category | string | Yes | The attachment's category: catalog, sfda_ar for the SFDA authorized-representative licence, iso for an ISO certificate, or authorization for the distributor authorization letter your organization holds from the manufacturer, which rides with its products in submission packs. The SFDA licence belongs in sfda_ar, never in authorization. |
filename | string | No | The original file name. Required unless you send staged_ref, in which case the held file's own name is used unless you rename it. |
content_type | string | No | The file's MIME type; required unless you send staged_ref. Accepted: application/pdf, images (image/jpeg, image/png, image/webp, image/gif), Office documents (Word, Excel, PowerPoint, old and OpenXML formats), text/plain and text/csv. |
size_bytes | integer | No | The file's size in bytes, if known, so a file over the limit is refused here instead of after the upload. |
staged_ref | string | No | A reference to a file the platform already holds for your organization, such as one sent to the WhatsApp assistant, listed by `whatsapp_attachments_list`. The file is copied into place inside the server, so there is nothing to upload: upload_url comes back empty and you go straight to the finalize tool. Send it instead of filename and content_type. |
{
"id": 214,
"category": "catalog",
"staged_ref": "wa-staged:3f9a1c52-7b1e-4d2a-9c61-5e0b2d7a8f14",
"filename": "meditech-catalog-2026.pdf"
}Returns: upload_id, upload_url, method (PUT), headers and expires_at (status 201). With staged_ref, upload_url is empty and staged is true.
- The limit is 15 MB for an uploaded file and 100 MB for a referenced one. A manufacturer that your organization does not own answers 404.
- The link takes one upload and expires at
expires_at, two hours after it was created. Nothing is filed before the finalize tool runs, and an upload not finalized within 24 hours is deleted along with its file. - An unsupported type answers 400 and a declared size over the limit answers 413; neither creates a link.
- Each organization, every API key and connector together, may hold at most 20 uploads open at once (started, not yet finalized, link unexpired) and start at most 500 a day (UTC), unless ProcDuck has set the organization other limits. Past either, the start tool answers 429 with
code: "upload_limit_reached"andretry_after_seconds: finalize the uploads whose file you sent, or wait that long. A link never used gives its place back when it expires.
manufacturer_attachment_upload_finalize
Finishes an upload started with manufacturer_attachment_upload_url_create: checks the file's real size and type, then files it on the manufacturer under the category chosen at the start.
Changes data · scope manufacturers.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
upload_id | string | Yes | The upload_id the matching start tool returned. |
{
"upload_id": "a41c9e27-6d03-4b8e-8f15-2c7a9b0d4e61"
}Returns: The new attachment's path, url (a link valid for about an hour) and name, with content_type and size_bytes.
- A
staged_refis used once: after a successful finalize the held copy is discarded, and if the file is refused the reference becomes usable again. - The server reads the file itself rather than trusting the declared type: a file that is not an accepted type, or is over the limit, is deleted and answers 422 or 413 with the reason, so start a new upload.
- Each
upload_idfinalizes once. An unknown one, or one from another organization, answers 404; an expired one answers 410; one already used answers 409. - Calling finalize before the upload answers 409 without spending it: upload the file to
upload_url, then call it again.
manufacturer_attachment_delete
Permanently deletes one attachment of a manufacturer. This cannot be undone.
Deletes or overwrites data · scope manufacturers.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The manufacturer's ID. |
category | string | Yes | The attachment's category, one of the values used when uploading: catalog, sfda_ar, iso or authorization. |
path | string | Yes | The attachment's path, from manufacturer_attachment_list. |
{
"id": 214,
"category": "iso",
"path": "9b2f1c0e-4d1a-4f6e-8c1b-3a7d5e2f9a10/manufacturers/214/attachments/iso/1791281700000-p7q2cd__iso-13485-certificate.pdf"
}Returns: A confirmation (success: true).
- A
paththat is not among this manufacturer's attachments of the givencategoryanswers 400, and a manufacturer your organization does not own answers 404.
Sales order documents
A sales order keeps its documents, first of all the customer's purchase order as a PDF. They are uploaded with the same pair of tools as every other file: a start, then a finish. The documents are listed in documents of `sales_orders_get`.
sales_order_document_upload_url_create
Starts filing a document on a sales order: the customer's purchase order as a PDF, a scan or photo of it, or a revised copy. Returns a temporary upload link, or copies a file the platform holds when you send staged_ref. Then call sales_order_document_upload_finalize.
Changes data · scope orders.write · roles: owner, admin, sales · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The sales order's ID, from sales_orders_list. |
filename | string | No | The original file name. Required unless you send staged_ref, in which case the held file's own name is used unless you rename it. |
content_type | string | No | The file's MIME type; required unless you send staged_ref. Accepted: application/pdf, images (image/jpeg, image/png, image/webp, image/gif), Office documents (Word, Excel, PowerPoint, old and OpenXML formats), text/plain and text/csv. |
size_bytes | integer | No | The file's size in bytes, if known, so a file over the limit is refused here instead of after the upload. |
staged_ref | string | No | A reference to a file the platform already holds for your organization, such as one sent to the WhatsApp assistant, listed by `whatsapp_attachments_list`. The file is copied into place inside the server, so there is nothing to upload: upload_url comes back empty and you go straight to the finalize tool. Send it instead of filename and content_type. |
{
"id": 1042,
"filename": "NUPCO-PO-4300010571.pdf",
"content_type": "application/pdf",
"size_bytes": 108281
}Returns: upload_id, upload_url, method (PUT), headers and expires_at (status 201). With staged_ref, upload_url is empty and staged is true.
- The limit is 15 MB for an uploaded file and 100 MB for a referenced one. An order keeps any number of documents. An order that your organization does not own answers 404.
- The link takes one upload and expires at
expires_at, two hours after it was created. Nothing is filed before the finalize tool runs, and an upload not finalized within 24 hours is deleted along with its file. - An unsupported type answers 400 and a declared size over the limit answers 413; neither creates a link.
- Each organization, every API key and connector together, may hold at most 20 uploads open at once (started, not yet finalized, link unexpired) and start at most 500 a day (UTC), unless ProcDuck has set the organization other limits. Past either, the start tool answers 429 with
code: "upload_limit_reached"andretry_after_seconds: finalize the uploads whose file you sent, or wait that long. A link never used gives its place back when it expires.
sales_order_document_upload_finalize
Finishes an upload started with sales_order_document_upload_url_create: checks the file's real size and type, then files it on the sales order.
Changes data · scope orders.write · roles: owner, admin, sales · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
upload_id | string | Yes | The upload_id the matching start tool returned. |
{
"upload_id": "a41c9e27-6d03-4b8e-8f15-2c7a9b0d4e61"
}Returns: The new document's path, url (a link valid for about an hour) and name, with content_type and size_bytes.
- The server reads the file itself rather than trusting the declared type: a file that is not an accepted type, or is over the limit, is deleted and answers 422 or 413 with the reason, so start a new upload.
- Each
upload_idfinalizes once. An unknown one, or one from another organization, answers 404; an expired one answers 410; one already used answers 409. - Calling finalize before the upload answers 409 without spending it: upload the file to
upload_url, then call it again.
sales_order_document_delete
Permanently deletes one document of a sales order. This cannot be undone.
Deletes or overwrites data · scope orders.write · roles: owner, admin, sales · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
id | integer | Yes | The sales order's ID. |
path | string | Yes | The document's path, from documents in sales_orders_get, up to 600 characters. |
{
"id": 1042,
"path": "9b2f1c0e-4d1a-4f6e-8c1b-3a7d5e2f9a10/sales-orders/1042/documents/1791281700000-p7q2cd__NUPCO-PO-4300010571.pdf"
}Returns: A confirmation (success: true).
- A
paththat is not among this order's documents answers 400, and an order your organization does not own answers 404.
Logo, stamp and signature
Three images print on quotations. The logo and the stamp belong to the organization and only the owner changes them; the signature is personal, and the member the connection acts as manages their own. Uploading always replaces the previous image.
settings_logo_upload_url_create
Starts setting the organization's logo, which prints in the header of quotation PDFs and replaces the previous logo once finalized. Returns a temporary upload link to send the image to with a PUT; then call settings_logo_upload_finalize.
Changes data · scope settings.write · roles: owner · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
filename | string | No | The original file name. Required unless you send staged_ref, in which case the held file's own name is used unless you rename it. |
content_type | string | No | The file's MIME type: image/png, image/jpeg, image/webp or image/svg+xml; image/gif, image/avif and image/tiff are accepted and converted on finalize. Required unless you send staged_ref. |
size_bytes | integer | No | The file's size in bytes, if known, so a file over the limit is refused here instead of after the upload. |
staged_ref | string | No | A reference to a file the platform already holds for your organization, such as one sent to the WhatsApp assistant, listed by `whatsapp_attachments_list`. The file is copied into place inside the server, so there is nothing to upload: upload_url comes back empty and you go straight to the finalize tool. Send it instead of filename and content_type. |
{
"filename": "acme-logo.png",
"content_type": "image/png",
"size_bytes": 48210
}Returns: upload_id, upload_url, method (PUT), headers and expires_at (status 201). With staged_ref, upload_url is empty and staged is true.
- The limit is 2 MB. A connection that does not belong to the organization's owner answers 403.
- The link takes one upload and expires at
expires_at, two hours after it was created. Nothing is filed before the finalize tool runs, and an upload not finalized within 24 hours is deleted along with its file. - An unsupported type answers 400 and a declared size over the limit answers 413; neither creates a link.
- Each organization, every API key and connector together, may hold at most 20 uploads open at once (started, not yet finalized, link unexpired) and start at most 500 a day (UTC), unless ProcDuck has set the organization other limits. Past either, the start tool answers 429 with
code: "upload_limit_reached"andretry_after_seconds: finalize the uploads whose file you sent, or wait that long. A link never used gives its place back when it expires.
settings_logo_upload_finalize
Finishes an upload started with settings_logo_upload_url_create: checks the image, converts, trims or downscales it where needed, then sets it as the organization's logo in place of the previous one.
Changes data · scope settings.write · roles: owner · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
upload_id | string | Yes | The upload_id the matching start tool returned. |
trim_whitespace | boolean | No | Crops near-white or transparent borders around the image, leaving padding of about 2% of the content's size. An SVG is left as it is. |
max_width | integer | No | Downscales the image to at most this many pixels wide, keeping its proportions; it never enlarges one. From 16 to 10000. |
{
"upload_id": "c2d8f1a0-4b7e-4e39-a5c6-1f0b9d2e7a43",
"trim_whitespace": true
}Returns: The saved image's path and url (a link valid for about an hour), with content_type, size_bytes, width, height, converted_from when the format was converted, trimmed and resized.
- An SVG is stored as uploaded, so
trim_whitespaceandmax_widthleave it alone.trim_whitespaceis often worth it for a logo or stamp exported with a wide margin. - A connection that does not belong to the organization's owner answers 403.
- An image in a format outside the allowed ones, such as AVIF or TIFF, is converted to JPEG at quality 85, or to PNG when it has transparency. An image that needs no conversion, trim or downscale is stored exactly as uploaded. HEIC is not accepted; export it as JPEG or PNG.
- The server reads the file itself rather than trusting the declared type: a file that is not an accepted type, or is over the limit, is deleted and answers 422 or 413 with the reason, so start a new upload.
- Each
upload_idfinalizes once. An unknown one, or one from another organization, answers 404; an expired one answers 410; one already used answers 409. - Calling finalize before the upload answers 409 without spending it: upload the file to
upload_url, then call it again.
settings_logo_delete
Removes the organization's logo, so quotation PDFs print without it. This cannot be undone; upload the logo again to restore it. Owner only.
Deletes or overwrites data · scope settings.write · roles: owner · Pro and Enterprise plans
No parameters.
Returns: A confirmation (success: true).
settings_stamp_upload_url_create
Starts setting the organization's stamp, which prints on quotation PDFs and replaces the previous stamp once finalized. Returns a temporary upload link to send the image to with a PUT; then call settings_stamp_upload_finalize.
Changes data · scope settings.write · roles: owner · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
filename | string | No | The original file name. Required unless you send staged_ref, in which case the held file's own name is used unless you rename it. |
content_type | string | No | The file's MIME type: image/png, image/jpeg, image/webp or image/svg+xml; image/gif, image/avif and image/tiff are accepted and converted on finalize. Required unless you send staged_ref. |
size_bytes | integer | No | The file's size in bytes, if known, so a file over the limit is refused here instead of after the upload. |
staged_ref | string | No | A reference to a file the platform already holds for your organization, such as one sent to the WhatsApp assistant, listed by `whatsapp_attachments_list`. The file is copied into place inside the server, so there is nothing to upload: upload_url comes back empty and you go straight to the finalize tool. Send it instead of filename and content_type. |
{
"filename": "acme-stamp.png",
"content_type": "image/png",
"size_bytes": 48210
}Returns: upload_id, upload_url, method (PUT), headers and expires_at (status 201). With staged_ref, upload_url is empty and staged is true.
- The limit is 2 MB. A connection that does not belong to the organization's owner answers 403.
- The link takes one upload and expires at
expires_at, two hours after it was created. Nothing is filed before the finalize tool runs, and an upload not finalized within 24 hours is deleted along with its file. - An unsupported type answers 400 and a declared size over the limit answers 413; neither creates a link.
- Each organization, every API key and connector together, may hold at most 20 uploads open at once (started, not yet finalized, link unexpired) and start at most 500 a day (UTC), unless ProcDuck has set the organization other limits. Past either, the start tool answers 429 with
code: "upload_limit_reached"andretry_after_seconds: finalize the uploads whose file you sent, or wait that long. A link never used gives its place back when it expires.
settings_stamp_upload_finalize
Finishes an upload started with settings_stamp_upload_url_create: checks the image, converts, trims or downscales it where needed, then sets it as the organization's stamp in place of the previous one.
Changes data · scope settings.write · roles: owner · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
upload_id | string | Yes | The upload_id the matching start tool returned. |
trim_whitespace | boolean | No | Crops near-white or transparent borders around the image, leaving padding of about 2% of the content's size. An SVG is left as it is. |
max_width | integer | No | Downscales the image to at most this many pixels wide, keeping its proportions; it never enlarges one. From 16 to 10000. |
{
"upload_id": "c2d8f1a0-4b7e-4e39-a5c6-1f0b9d2e7a43",
"trim_whitespace": true
}Returns: The saved image's path and url (a link valid for about an hour), with content_type, size_bytes, width, height, converted_from when the format was converted, trimmed and resized.
- An SVG is stored as uploaded, so
trim_whitespaceandmax_widthleave it alone.trim_whitespaceis often worth it for a logo or stamp exported with a wide margin. - A connection that does not belong to the organization's owner answers 403.
- An image in a format outside the allowed ones, such as AVIF or TIFF, is converted to JPEG at quality 85, or to PNG when it has transparency. An image that needs no conversion, trim or downscale is stored exactly as uploaded. HEIC is not accepted; export it as JPEG or PNG.
- The server reads the file itself rather than trusting the declared type: a file that is not an accepted type, or is over the limit, is deleted and answers 422 or 413 with the reason, so start a new upload.
- Each
upload_idfinalizes once. An unknown one, or one from another organization, answers 404; an expired one answers 410; one already used answers 409. - Calling finalize before the upload answers 409 without spending it: upload the file to
upload_url, then call it again.
settings_stamp_delete
Removes the organization's stamp, so quotation PDFs print without it. This cannot be undone; upload the stamp again to restore it. Owner only.
Deletes or overwrites data · scope settings.write · roles: owner · Pro and Enterprise plans
No parameters.
Returns: A confirmation (success: true).
settings_signature_get
Returns the signature image of the member the connection acts as, or nulls when none is stored. The signature belongs to the member, not to the organization.
Read-only · scope settings.read · all roles · Pro and Enterprise plans
No parameters.
Returns: The path and url of the signature, the link valid for about an hour, or null for both when there is none.
settings_signature_upload_url_create
Starts setting the signature image of the member the connection acts as, which prints on approved quotations that name them as sales person and replaces any earlier signature once finalized. Returns a temporary upload link to send the image to with a PUT; then call settings_signature_upload_finalize.
Changes data · scope settings.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
filename | string | No | The original file name. Required unless you send staged_ref, in which case the held file's own name is used unless you rename it. |
content_type | string | No | The file's MIME type: image/png, image/jpeg, image/webp or image/svg+xml; image/gif, image/avif and image/tiff are accepted and converted on finalize. Required unless you send staged_ref. |
size_bytes | integer | No | The file's size in bytes, if known, so a file over the limit is refused here instead of after the upload. |
staged_ref | string | No | A reference to a file the platform already holds for your organization, such as one sent to the WhatsApp assistant, listed by `whatsapp_attachments_list`. The file is copied into place inside the server, so there is nothing to upload: upload_url comes back empty and you go straight to the finalize tool. Send it instead of filename and content_type. |
{
"filename": "signature.png",
"content_type": "image/png",
"size_bytes": 48210
}Returns: upload_id, upload_url, method (PUT), headers and expires_at (status 201). With staged_ref, upload_url is empty and staged is true.
- The limit is 2 MB.
- The link takes one upload and expires at
expires_at, two hours after it was created. Nothing is filed before the finalize tool runs, and an upload not finalized within 24 hours is deleted along with its file. - An unsupported type answers 400 and a declared size over the limit answers 413; neither creates a link.
- Each organization, every API key and connector together, may hold at most 20 uploads open at once (started, not yet finalized, link unexpired) and start at most 500 a day (UTC), unless ProcDuck has set the organization other limits. Past either, the start tool answers 429 with
code: "upload_limit_reached"andretry_after_seconds: finalize the uploads whose file you sent, or wait that long. A link never used gives its place back when it expires.
settings_signature_upload_finalize
Finishes an upload started with settings_signature_upload_url_create: checks the image, converts, trims or downscales it where needed, then sets it as the member's signature in place of the previous one.
Changes data · scope settings.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
upload_id | string | Yes | The upload_id the matching start tool returned. |
trim_whitespace | boolean | No | Crops near-white or transparent borders around the image, leaving padding of about 2% of the content's size. An SVG is left as it is. |
max_width | integer | No | Downscales the image to at most this many pixels wide, keeping its proportions; it never enlarges one. From 16 to 10000. |
{
"upload_id": "c2d8f1a0-4b7e-4e39-a5c6-1f0b9d2e7a43",
"trim_whitespace": true
}Returns: The saved image's path and url (a link valid for about an hour), with content_type, size_bytes, width, height, converted_from when the format was converted, trimmed and resized.
- The signature prints when the sales person linked to the quotation is a team member tied to their account on the sales people list, and approval never waits for a signature. A signature photographed on white paper usually wants
trim_whitespace. - Only the member who started a signature upload can finalize it.
- An image in a format outside the allowed ones, such as AVIF or TIFF, is converted to JPEG at quality 85, or to PNG when it has transparency. An image that needs no conversion, trim or downscale is stored exactly as uploaded. HEIC is not accepted; export it as JPEG or PNG.
- The server reads the file itself rather than trusting the declared type: a file that is not an accepted type, or is over the limit, is deleted and answers 422 or 413 with the reason, so start a new upload.
- Each
upload_idfinalizes once. An unknown one, or one from another organization, answers 404; an expired one answers 410; one already used answers 409. - Calling finalize before the upload answers 409 without spending it: upload the file to
upload_url, then call it again.
settings_signature_delete
Removes the signature of the member the connection acts as. Quotations that name this member as sales person then print without a signature, and approval is never blocked by a missing one. This cannot be undone.
Deletes or overwrites data · scope settings.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
No parameters.
Returns: A confirmation (success: true).
Importing from a spreadsheet
sheet_import reads an Excel or CSV file on the server: it works out which column means what from a small sample, then reads every row itself, so a sheet of 2,000 rows costs what a sheet of 70 does. It has five targets. tender_items creates a manual tender of your organization with the rows as its items. products creates catalog products and links each to the manufacturer named in its row, creating the manufacturer when it is new. quotation_items creates one quotation, and every line needs a price from the file, so a sheet with no price column comes back as needs_confirmation. bid_pricing prices an existing tender's items by matching material codes. manufacturers creates one entry per row and skips names you already have, listing them in skipped_existing. A short list, a PDF or a photo does not need this tool: create the records with `tenders_create`, `products_bulk_create` or `quotations_create`.
sheet_import
Creates records from an Excel or CSV file without you reading its rows. The server works out from a sample which column means what, applies that to every row and returns the mapping it used. When it is unsure, it writes nothing and returns the mapping for you to confirm.
Changes data · scope tenders.write or products.write or quotations.write or bids.write or manufacturers.write · roles: owner, admin, sales, purchasing · Pro and Enterprise plans
| Parameter | Type | Required | Description |
|---|---|---|---|
sheet_base64 | string | Yes | The content of an .xlsx or .csv file as base64. About 3 MB decoded is the practical ceiling, because the server refuses any request over about 4.5 MB. Send the file's own bytes, not a table rebuilt from its rows. On WhatsApp the assistant passes the attachment's reference in place of the content; the server reads the file and keeps it available for the confirming call. |
filename | string | No | The original file name, up to 300 characters. |
target | string | Yes | What the rows become: tender_items for the items of a new manual tender (send tender), products for catalog products, quotation_items for the lines of a new quotation (send quotation), bid_pricing for prices on an existing tender's items (send tender_id), manufacturers for entries in your manufacturer directory. |
instruction | string | No | What the file's owner said about it, in their own words, for example that the description column is the item name and the specification column is the description. It outranks the automatic reading of the columns. Up to 2,000 characters. |
sheet | string | No | The worksheet's name. The first worksheet is used when omitted, and the reply lists the others in other_sheets. An unknown name answers 400. |
layout | object | No | The mapping you state yourself instead of having the server detect it: the header row, the data range and what each column feeds. Use it to correct a column after the file's owner has reviewed the mapping, or when you already know it. Every reply returns the mapping it used in this same shape, so you can send it back after editing it. A mapping you send is taken as stated and needs no confirmation. |
layout.sheet | string | No | The worksheet this mapping applies to, up to 200 characters. |
layout.header_row | integer | Yes | The header row's number, counting from 1 as Excel does, or 0 when the sheet has no headers. Required when layout is sent. |
layout.data_start_row | integer | Yes | The number of the first real data row, counting from 1. Required when layout is sent. |
layout.data_end_row | integer | Yes | The number of the last data row, or 0 to run to the end of the sheet. Required when layout is sent. |
layout.columns | object[] | Yes | The columns and the field each one feeds, from 1 to 60 columns. Required when layout is sent. |
layout.columns[].column | string | Yes | The column's letter in the sheet: A, B, AA. |
layout.columns[].field | string | Yes | The field this column feeds, or ignore to skip it. Fields of tender_items: item_name, description, quantity, unit, estimated_price, material_code. Fields of products, each with the column header the template and the export give it: product_name: Product name; description: Description; nupco_product_code: NUPCO code; catalogue_number: Catalogue number; manufacturer_name: Manufacturer; country_of_origin_code: Country of origin; price_basis: Price per; cost_price: Cost price; cost_currency: Cost currency; sale_price_sar: Sale price (SAR); vat_percent: VAT %; case_type: Case type; units_per_case: Units per case; min_order_qty: Minimum order quantity; is_medical: Medical; mdma_number: MDMA number; mdma_expiry: MDMA expiry; mdma_status: MDMA status; listing_number: Listing number; shelf_life_years: Shelf life (years); first_delivery_days: First delivery (days); order_delivery_days: Order delivery (days); unit_of_measure: Unit of measure; weight_kg: Unit weight (kg); length_cm: Unit length (cm); width_cm: Unit width (cm); height_cm: Unit height (cm); case_weight_kg: Case weight (kg); case_length_cm: Case length (cm); case_width_cm: Case width (cm); case_height_cm: Case height (cm); package_type: Package type; package_size: Package size; inner_length_cm: Pack length (cm); inner_width_cm: Pack width (cm); inner_height_cm: Pack height (cm); volume: Volume; volume_unit: Volume unit; content_quantity: Content per unit; content_unit: Content unit; packing_kind: Product kind; foc_percent: FOC %; additional_info: Additional info; search_query: Search query; photo_files: Photo files; catalog_files: Catalog files. Fields of quotation_items: description, quantity, unit_price, unit, stock_no, catalog_no, vat_rate. Fields of bid_pricing: material_code, unit_price, notes. Fields of manufacturers: name, contact_person, country, email, phone, website, address, notes, agency_type. Two columns may feed the same field (a specification in two languages) and are joined in the order sent. A field that is not one of the target's fields is refused. |
layout.columns[].header | string | No | The column's own header text, shown when the mapping is reviewed. |
confirm | boolean | No | Accepts a mapping that came back as needs_confirmation. Send it only after the file's owner has seen the mapping. |
dry_run | boolean | No | Reads the file and counts the rows without writing anything, and returns the mapping and a preview with the status preview. |
tender | object | No | The header of the manual tender that will hold the items. Required with tender_items; take it from what the file's owner said or from the file itself. |
tender.tender_name | string | Yes | The tender's name, up to 500 characters. Required when tender is sent. |
tender.agency_name | string | Yes | The buying agency's name, up to 300 characters. Required when tender is sent. |
tender.tender_number | string | No | The tender number, up to 100 characters. Send null to leave it empty. |
tender.reference_number | string | No | The tender's reference number, up to 100 characters. Send null to leave it empty. |
tender.branch_name | string | No | The branch name, up to 200 characters. Send null to leave it empty. |
tender.last_offer_date | string | No | The offer submission deadline as an ISO 8601 timestamp, such as 2026-11-02T12:00:00. Send null to leave it empty. |
manufacturer_id | integer | No | Links every product created to this manufacturer, with the products target. From manufacturers_list. |
quotation | object | No | The header of the quotation that will hold the lines, with quotation_items: customer, tender link, currency and validity. Take it from what the file's owner said; the line prices come from the sheet. |
quotation.tender_id | integer | No | Links the quotation to a tender, and the tender's agency becomes the addressee. Send null for a quotation with no tender. |
quotation.customer_name | string | No | The customer's name, up to 200 characters. Send null to leave it empty. |
quotation.rfq_no | string | No | The customer's RFQ number, up to 100 characters. Send null to leave it empty. |
quotation.quote_validity_days | integer | No | How long the quotation stays valid, in days, from 1 to 3650. Send null to leave it empty. |
quotation.delivery_period_days | integer | No | The delivery period in days, from 1 to 3650. Send null to leave it empty. |
quotation.currency | string | No | The currency code, 1 to 8 characters. Defaults to SAR. |
quotation.notes | string | No | Notes on the quotation, up to 2,000 characters. Send null to leave them empty. |
quotation.sales_person_id | integer | No | The sales person printed on the quotation, from sales_persons_list. Send null to leave it empty. |
tender_id | integer | No | The tender whose items a bid_pricing sheet prices. Required with that target, from tenders_list. Each row is matched to a live item by material_code alone, an exact match that ignores letter case. A row with no item, or whose code two items share, is not priced and comes back in unmatched with its row number; price those with `bid_pricing_set_batch`. A row priced 0 keeps its price and clears the item's participation mark, as in `bid_pricing_set_batch`. |
constants | object | No | Values the file's owner stated once for the whole sheet, applied to every row: for example {"cost_currency": "SAR"} when prices are in riyals and no column carries the currency. A constant for a field that a column already feeds is refused, and the reply echoes the constants applied so you can read them back. |
files | string[] | No | For the products target only: the names of the photo and catalog files you hold for this sheet, up to 3,000, names only and never their content. The reply's attachments then says which file goes to which row: by the Photo files and Catalog files cells, else by files named after the row's catalogue number. Without it, attachments lists the names the cells ask for without checking them against files. |
{
"sheet_base64": "UEsDBBQABgAIAAAAIQBi7p1L...",
"filename": "meditech-price-list.xlsx",
"target": "products",
"manufacturer_id": 214,
"instruction": "The price column is the cost per carton; the prices are in USD",
"constants": {
"cost_currency": "USD"
},
"dry_run": true
}Returns: An object with the status (imported, preview or needs_confirmation), the mapping used in mapping, the ignored columns, the row counts in counts, the first 20 problems in issues with their row numbers, a 3-row preview and the layout in a form you can send back. Once something is written it adds created, with tender_id for the tender, quotation_id and quote_number for the quotation, priced and unmatched for prices, and skipped_existing for manufacturers. For products, own_headers: true says the mapping was read from the headers, and attachments carries the file plan: rows (each row's photos and catalogs, with product_id once imported), problems per row and file, shared_catalogue_numbers and unused files.
- A status of
needs_confirmationmeans the server is unsure of its reading of the columns, or a required field has no column, and nothing is written. Show the mapping to the file's owner in their language, then call again withconfirm: trueor a correctedlayout. - Each target has its own scope:
tender_itemsneedstenders.writeand the owner or admin role,productsneedsproducts.write,manufacturersneedsmanufacturers.write,quotation_itemsneedsquotations.writeandbid_pricingneedsbids.write. The last four are open to the owner, admin, sales and purchasing roles. - Reading skips empty rows, repeated header rows, merged banner rows such as a page total, and rows missing a required field, and counts them by reason in
counts. Rows beyond the target's cap are not written and are counted incounts.truncated: 2,000 fortender_itemsandbid_pricing, 500 forproducts,quotation_itemsandmanufacturers. - With the
productstarget every product field the app edits can be a column, plus Photo files and Catalog files. A sheet whose headers are exactly the import's own, such as the template from `products_import_template` or the output of `products_export_csv`, is mapped from its headers with no detection step. A column holding a list separates it with a semicolon (;): NUPCO codes with the primary first, manufacturers, and file names. - A Price per column says what the row's prices are for: a level word (Piece or Unit for the piece, Pack or Box for the pack, Carton or Case for the case, Content for one unit of what a piece holds, or the Arabic words for them), or the name of one of the row's own levels, such as Vial or Kit. A cell naming no level of the row, or a level the row has no count for, drops the level and both prices together, each with an issue and its row number, because a price without its level says nothing about what unit it is for. A blank cell leaves the level not stated, so the prices are used as they are on every tender line. A Content unit column reads the content unit by any of its names (ml, mL, Tests, or its Arabic name), and an unknown one drops both content cells; a Product kind column reads a type's key or its English or Arabic name.
- If the write is still refused for packing (
price_basis_level_missingorcontent_incomplete), nothing is written androwsnames each refused row by its sheetrow, the field and the fix. - To attach the files after the import, use the
product_idinattachments.rows: `product_photo_upload_url_create` for photos, and `product_attachment_upload_url_create` with categorycatalogfor catalogs, each followed by its finalize tool. Upload a file several rows share once, then copy it with `product_files_copy`. - The tool takes spreadsheets only. A PDF, Word or PowerPoint file answers 400, and an old xls file answers 400 too and must be saved as xlsx or csv. Creating products past your plan's limit answers 403, and creating the quotation counts against the plan's monthly limit on new quotations.