Skip to content

Importing products from CSV

Adding products one at a time is the right shape for a handful of changes. A whole catalog is a different job, and POST /api/v1/products/import does it in one request: a CSV of up to 500 products, validated as a unit and written in a single transaction that also records the background indexing those products need before they can match.

Use the import when you are onboarding a catalog, taking on a new manufacturer partner’s product line, or reconciling what Prokure holds against the spreadsheet you already maintain. Use Adding a product for a single product, and PATCH for a single edit. The import has no specs column, so a product whose specifications matter still needs one API call or one portal edit after it lands.

Terminal window
curl "https://app.prokure.ca/api/v1/products/import/template" \
-H "Authorization: Bearer $PROKURE_API_KEY" \
-o product-import-template.csv

Requires profile:read. It returns a text/csv body: the header row plus one example row.

oem_partner,name,model,description,commodity_codes,rep_status,source_url
Vertex Optronics,Thermal Monocular,VX-300,"Handheld thermal imager, 640x480 sensor, 50 Hz refresh",5855;5855-20,carried,https://example.com/products/vx-300

All seven columns have to be present. Their order does not matter, and header cells are matched without regard to case or surrounding whitespace, so a spreadsheet that exports OEM_Partner is read the same as one that exports oem_partner.

The file has to be UTF-8. A byte-order mark is fine, and so is any accented or non-Latin text the encoding covers, but any other encoding is refused: a UTF-16 or Windows-1252 export is rejected with file is not UTF-8 encoded; save the spreadsheet as CSV UTF-8 rather than imported with its accented characters mangled. In Excel that means saving as CSV UTF-8, not plain CSV.

Column Required Rule
oem_partner Yes The manufacturer partner’s name, 1 to 200 characters. Matched against your existing partners without regard to case; a name you do not have yet is created.
name Yes The product name, 1 to 200 characters.
model No Up to 100 characters. Leave it blank for a product with no model.
description No Up to 4000 characters. This is what a solicitation’s requirements are compared against, so it carries most of the matching weight.
commodity_codes No Up to 50 codes separated by ;, each 1 to 50 characters.
rep_status No carried or catalog, read without regard to case. Blank means carried.
source_url No An http or https URL.

A cell that breaks one of those rules is reported with the rule as its message:

Column Message
oem_partner oem_partner is required (1–200 characters)
name name is required (1–200 characters)
model model must be at most 100 characters
description description must be at most 4000 characters
commodity_codes commodity_codes: at most 50 codes, each 1–50 characters, separated by ;
rep_status rep_status must be carried or catalog
source_url source_url must be an http or https URL

A header problem is reported on row 1, against the column it concerns: missing required column for one of the seven that is absent, unknown column for a column that is not one of the seven, and duplicate column for one that appears twice.

The header is row 1 and the first product is row 2, so the numbers in an issue list are the row numbers your spreadsheet shows. A description quoted across several lines still reports the line its row starts on, and blank lines are skipped without shifting the count.

dry_run=true runs the whole validation and reports what the import would do, without writing anything:

Terminal window
curl -X POST "https://app.prokure.ca/api/v1/products/import?dry_run=true" \
-H "Authorization: Bearer $PROKURE_API_KEY" \

Requires profile:write. The request is multipart/form-data with one file field. A valid file comes back as a preview:

{
"dry_run": true,
"rows": 128,
"partners_to_create": ["Vertex Optronics", "Halden Marine", "Kestrel Instruments"],
"partners_to_reauthorize": ["Cascade Fabrication"],
"products_to_create": 112,
"products_to_update": 16
}
Field Meaning
rows Data rows in the file, header excluded.
partners_to_create The names of the manufacturer partners the import would create, spelled as the file spells them.
partners_to_reauthorize The names of partners you already have but have removed from your company profile, spelled as your catalog already spells them.
products_to_create Rows that do not match a product you already have.
products_to_update Rows whose partner, name and model already match one.

The two partner fields are lists of names rather than counts, so a preview shows you exactly which partners a file is about to introduce. A name you did not expect to see there is usually a typo in the spreadsheet: it reads as a new partner rather than as a row under an existing one.

Read products_to_update before you commit. Every row it counts is about to be overwritten from the file on the terms in Re-importing. A number larger than you expected usually means the file names products you have already onboarded, not that the import is wrong.

An import either applies in full or changes nothing. Any of these refuses the whole file with 422 invalid_import:

  • a file that is not UTF-8
  • a header column missing, unknown, or repeated
  • any cell that breaks its column’s rule
  • the same partner, name and model appearing on two rows, reported on the second as duplicates row 12 naming the first. The partner is compared without regard to case, and the name and model exactly, which is the identity your catalog itself enforces
  • more than 500 rows, reported on the first row past the cap as at most 500 rows per import

The body lists what to fix:

{
"error": "invalid_import",
"message": "3 issue(s) in the file; nothing was imported",
"requestId": "req-9c2",
"issues": [
{ "row": 14, "column": "rep_status", "message": "rep_status must be carried or catalog" },
{ "row": 21, "column": "source_url", "message": "source_url must be an http or https URL" },
{ "row": 37, "column": null, "message": "duplicates row 12" }
],
"issue_count": 3
}

An encoding failure is decided before anything is parsed, so it arrives alone, on row 1, as the file’s only issue. Everything else is checked across the whole file at once.

issues carries at most the first 50 problems; issue_count is how many there really are, and it is the number the message quotes, so a file with 62 problems reports 62 while listing 50. A column of null means the problem is the row itself rather than one of its cells, which is what a duplicate row and the 500-row cap both report. Fix the file and send it again: nothing was written, so there is no half-finished import to clean up first.

Drop dry_run to commit:

Terminal window
curl -X POST "https://app.prokure.ca/api/v1/products/import" \
-H "Authorization: Bearer $PROKURE_API_KEY" \
{
"dry_run": false,
"partners_created": 3,
"partners_reauthorized": 1,
"products_created": 112,
"products_updated": 16,
"profile_changes_applied": 4,
"profile_changes_failed": 0
}

Partners and products are written in one transaction, so a failure anywhere leaves your catalog exactly as it was. profile_changes_applied counts the entries the import wrote to your profile’s change history, one for each manufacturer in the file that was missing from manufacturer_partners on your company profile: the four above are the three created partners and the one re-authorized, none of which were on the profile beforehand.

Every import checks all of the file’s manufacturers against the profile, not only the ones it created that run. A name that is already there is left alone, and a name that should be there but is not gets added by the next import that mentions it.

That reconciliation is also the repair path when the profile half goes wrong. It runs after the catalog transaction has committed, so your products are in either way, and a name that fails to reach the profile is counted in profile_changes_failed rather than turned into an error: the response is still a 200 reporting everything that was imported. A non-zero profile_changes_failed is worth acting on, because manufacturer_partners is one of the things Prok scores against, and the action is simply to import the same file again.

Field Meaning
partners_created Partner names in the file that you did not have.
partners_reauthorized Partners you had removed from your company profile, added back by this import.
products_created Rows that did not match an existing product.
products_updated Rows that matched one, and overwrote it.
profile_changes_applied Entries written to your profile’s change history, one per manufacturer in the file that was missing from manufacturer_partners and got added. A name already on the profile is not added twice, so this can come back lower than partners_created plus partners_reauthorized, and it can also count a partner this import neither created nor re-authorized.
profile_changes_failed Manufacturers from the file that could not be added to your company profile. The products themselves were imported; importing the same file again repairs the profile. Normally 0.

A product is matched on its partner, name and model, which is the same identity POST /api/v1/products refuses a duplicate on. A row that matches an existing product updates it rather than creating a second one, so re-importing a corrected spreadsheet is a supported way to work.

What that overwrite covers is worth being precise about, because a blank cell is an instruction, not an omission:

Column On a re-import
description Overwritten from the file. A blank cell clears the stored description.
commodity_codes Overwritten from the file. A blank cell clears the stored codes.
rep_status Overwritten from the file. A blank cell sets carried.
source_url Overwritten from the file. A blank cell clears the stored URL.
specs Preserved. The file has no column for it.
status Preserved. A discontinued product stays discontinued after a re-import.

The file is authoritative for everything it carries, so build it from what you currently hold rather than from only the rows you meant to change. GET /api/v1/products lists the catalog as it stands. A file assembled from a handful of edited rows, with the other columns left empty, clears those columns on every product it touches.

status staying put is the deliberate exception. Discontinuing a product is a decision you made about your catalog, and a re-import of the same product line must not quietly bring it back into scoring. Reactivate one with PATCH /api/v1/products/{id} when you mean to.

A partner name in the file is matched against your existing partners without regard to case, and the spelling you already have on file is the one kept, so vertex optronics in a spreadsheet does not rename Vertex Optronics in your catalog.

The same rule settles a file that spells a new partner two ways. Rows saying Halden Marine and halden marine are one partner, not two, and the spelling on the first of those rows is the one created and used for every row under it. A file whose casing is inconsistent therefore imports cleanly; it just inherits whichever spelling happens to come first, so it is worth spotting in the dry run’s partners_to_create before you commit.

A name you do not have is created, and added to manufacturer_partners on your company profile through the ordinary change history, with source set to portal_import and shown in the portal as You — catalog import. Those entries are revertible on the usual terms, the same as a partner you added by hand. A partner you had removed from your profile is re-authorized and added back, rather than blocking the import: an import is a clear enough statement that you represent the partner again.

See Company profile for the history and revert rules those entries land in.

The transaction that writes the catalog also records two pieces of background work: the indexing the imported products need, and one re-score for the whole catalog change rather than one per product. Nothing is indexed while the request is open, so a 200 means the catalog is saved and that work is durably recorded, not that indexing has finished. Indexing runs in the background in bounded batches, and the imported products count toward matching once it finishes. The re-scored results reach you in your next digest rather than in an email of their own, exactly like a company-profile edit.

The Product catalog page under Settings has an Import button. It opens a dialog that takes you through the same three steps: choose a file, read the dry-run preview, then import. A valid file previews as the counts above; an invalid one shows the issue table, with the row, the column and the message for each problem, so the file can be fixed and re-chosen without leaving the dialog. The dialog also links the template.

After an import it lists the same counts, and warns when profile_changes_failed is not zero, telling you how many manufacturers did not reach your company profile and to import the file again to repair it.

  • One file per request, a .csv of at most 2 MB and at most 500 data rows. A larger catalog goes in as several files.
  • Cell limits are the catalog’s limits: name up to 200 characters, model up to 100, description up to 4000, up to 50 commodity codes of up to 50 characters each.
  • 50 issues per response. issue_count reports the true total when there are more.
  • 10 requests per minute on the import and 60 per minute on the template, counted per client IP like every other route. See Rate limits and errors.

Four failures are specific to this route:

Status error Meaning
400 missing_file The request carried no file field.
413 file_too_large The file is over 2 MB, or the request carried more multipart parts than the route accepts.
415 unsupported_type The file is not a .csv.
422 invalid_import The file did not validate. Nothing was written.

The usual 401, 403 and 429 apply on top of them, on the same terms as every other route.