Making Products Available

Note

Part of the Wholesaler Integrations guide. This step assumes you have already completed Registering an Available Wholesaler — catalog uploads are checked against a specific clinic’s Wholesaler and its linked AvailableWholesaler.

Once a clinic has connected your wholesaler (and you have received its wholesaler.created webhook), you can push a product catalog for that clinic. Clinic staff later import the catalog into their own stock from the Provet UI.

A CSV file is the only way to provide a catalog. There is no endpoint that accepts individual products one at a time, and no other file format is accepted — every product you want a clinic to be able to import must be a row in the CSV described below, uploaded as a whole file in one go (see CSV column reference).

A minimal example file, using only the required columns:

product_type,product_number,product_name,wholesale_price,vat
Medicine,ACM-001,Amoxicillin 250mg,4.50,24
Supply,ACM-200,Surgical Gloves M,0.80,14

Step 1 — Request a presigned upload URL

# pk = the Wholesaler ID received in the wholesaler.created webhook.
curl -X POST \
  "https://[env.]provetcloud.com/<provet_id>/api/0.1/wholesaler/<wholesaler_pk>/catalog_upload_url/" \
  -H "Authorization: Bearer <oauth_access_token>" \
  -H "Content-Type: application/json" \
  -d '{"filename": "acme-catalog-2026-06.csv"}'

Response:

{
  "upload_id": 55,
  "filename": "acme-catalog-2026-06.csv",
  "s3_key": "<opaque S3 object key>",
  "url": "<presigned S3 upload URL>",
  "fields": {
    "key": "<opaque S3 object key>",
    "AWSAccessKeyId": "...",
    "policy": "...",
    "signature": "...",
    "x-amz-security-token": "..."
  },
  "expected_columns": ["product_type", "product_number", "..."]
}

s3_key, url, and fields are opaque — pass them through as-is to the S3 upload in Step 2 without parsing or constructing them yourself. fields is not a fixed set: submit every key-value pair it returns, not just the ones shown here.

Authorization check. The target must be an active integration wholesaler — not archived, and created from an AvailableWholesaler (Registering an Available Wholesaler). Checks run in order, so the failure you see depends on which condition is not met:

  • Not an active integration wholesaler (does not exist, archived, or a wholesaler registered with a type other than Integration, such as Email, with no linked AvailableWholesaler): 404 (No Wholesaler matches the given query.).

  • An active integration wholesaler whose linked AvailableWholesaler belongs to a different OAuth application: 403 (Token does not match this wholesaler.).

Filename validation: must end in .csv, no path traversal (/, \, ..).

Step 2 — Upload the CSV directly to S3

curl --request POST \
  --url "<url from step 1>" \
  --form "key=<key from fields>" \
  --form "AWSAccessKeyId=<AWSAccessKeyId from fields>" \
  --form "policy=<policy from fields>" \
  --form "signature=<signature from fields>" \
  --form "x-amz-security-token=<x-amz-security-token from fields, if present>" \
  --form "file=@/path/to/acme-catalog-2026-06.csv"

Include every field returned under fields in Step 1 as its own --form entry, in the order S3 returned them, with file last. Do not set a Content-Type: multipart/form-data header yourself — curl --form generates the multipart body and its matching boundary for you, and a manually-set header without that boundary breaks the upload.

Note

Nothing needs to be reported back to Provet once the upload completes — Provet is notified automatically by S3 and proceeds with the product import step once a clinic imports it. The CSV itself is not read or validated at upload time; parsing and row-level validation happen at import time (Clinic-side import), so a malformed CSV uploads successfully here and only surfaces as an error later.

CSV column reference

Each row describes one product. A header row is mandatory; columns are matched by name and may appear in any order. Files are read as UTF-8 (a byte-order mark is tolerated), values are trimmed of surrounding whitespace, and a blank optional cell is treated as “not set”.

Required columns

Every row must include these five columns, or the row is rejected:

Column

Type

Meaning

product_type

string

Product category: Food, Medicine, Supply, or Unknown (case-insensitive; blank becomes Unknown). Any other value rejects the row. Selects which stock item type the row is matched against; Unknown matches across all types.

product_number

string

Your product code / SKU. Primary matching key: matched against each item’s primary, then secondary, then tertiary wholesaler code, and written to the item’s wholesaler code on import.

product_name

string

Product display name. Stored on the matched item; used for search and sorting in the import UI.

wholesale_price

decimal

Purchase price excluding VAT (0 allowed). Becomes the item’s wholesale price and drives price-change detection. Falls back to product_price if blank.

vat

decimal

VAT rate as a percentage (e.g. 24), not a fraction. Matched to the item’s VAT group percentage.

Optional columns

Column

Type

Meaning

product_uom

string

Unit of measure / package description, e.g. bottle or box of 10. Stored as the item’s package description.

product_barcode

string

Product barcode (EAN / GTIN). Stored on the item; used as a lowest-priority matching key when barcode matching is enabled.

product_price

decimal

Recommended retail price excluding VAT. Pre-populates the item’s selling price, and is the fallback source for wholesale_price when that is blank.

currency_code

string

ISO 4217 currency of the prices, e.g. EUR. Descriptive metadata; not validated.

dosage_units

decimal

Total dosable units across all packages. If omitted or not greater than 0, computed as unit_size × wholesale_package_size, ultimately defaulting to 1.0.

unit_size

decimal

Dosable units in a single package (e.g. 100 for a 100 ml bottle). Only used as an input to dosage_units; values of 0 or less are ignored.

wholesale_package_size

decimal

Number of packages in one wholesale unit (e.g. 3 for a 3×100 multipack). Only used as an input to dosage_units; values of 0 or less are ignored.

active_substance

string

Active pharmaceutical ingredient, for medicines.

administration_method

integer

Route-of-administration code for medicines (133; e.g. 1 = CRI, 14 = IM, 19 = IV, 23 = PO, 25 = SC, 27 = Topical). An unrecognized value rejects the row.

drug_strength

decimal

Strength / concentration of the active substance, for medicines.

Note

wholesale_price is the clinic’s purchase cost; product_price is the suggested resale price. Matching is always scoped by product_type, then resolved by product_number (primary, then secondary, then tertiary wholesaler code), with barcode as a fallback when barcode matching is enabled.

Clinic-side import

Importing the uploaded catalog into stock is a manual step clinic staff perform later from the Provet UI, under Settings > Import & Export > Import from Lists — it is not something your system calls or needs to wait for. Clinic staff filter by wholesaler and item type, review new items (highlighted) and updates to existing items (compared side-by-side) in the generated list, then assign a sub-group, VAT group, and markup before finalizing the import.

See Import or Update Items from Integrated Wholesalers for the full customer-facing walkthrough of this screen. There is currently no way for your integration to be notified of, or query, problems found at import time (e.g. a malformed row) — those surface only in the clinic’s own import UI.

See the full error reference for the errors your own API calls can receive.