Skip to content

Seller API guide

This guide is for a Zentail seller scripting their own account. You are not building an application other sellers install. You are automating your own Zentail data, usually the import and export work you would otherwise do by hand in the UI.

There is only a v1 API for this profile today. It is also the one Zentail profile that does not use OAuth. You generate a token for yourself in account settings, so there is no grant, no client ID and no redirect to implement.

Scripts that work through the profile’s eight operations, in four groups:

  • Reports: run a product data import or export, and read the results back.
  • CSV import validation: check a file parses and passes validation before you commit to importing it.
  • Stock: read the total available quantity for one SKU.
  • Custom order integrations and vendors: two standalone id lookups.

You normally need Account Admin permissions. The API Access tab shows for an Account Admin, and also for a user who manages other users. Without either, the tab does not appear and there is nothing to generate. Zentail’s help centre covers how to manage user permissions.

If you are building something several sellers will install, this is the wrong profile. You want a partner application and the authorization-code flow instead.

In Zentail:

  1. Go to Account Settings.
  2. Open the API Access tab.
  3. Click GENERATE NEW TOKEN.
  4. In the Token Details dialog, enter a contact email and a label, then click GENERATE. The label is how you tell your tokens apart later.

The new token appears in the Active Tokens table under that label. Treat it as a password: it is your whole account, and anyone holding it can read and write your products.

You can generate as many tokens as you need. There is no cap, and each one carries its own label and contact email and can be revoked on its own.

A seller token carries no application identity — it belongs to the seller, not to any installed app — and can be regenerated in account settings without notice to anyone depending on it. Don’t cache or key long-lived tooling off one without accounting for that.

The token carries a single scope, legacy, which opens the v1 endpoints and nothing else. Scopes lists every scope and the routes each one opens.

Put the token in the Authorization header as the whole header value, with no Bearer prefix. Zentail matches the header verbatim against the token it issued, so a prefixed header is rejected.

Terminal window
curl -X GET "https://api.zentail.com/v1/simpleInventory/TESTSKU1" \
-H "Authorization: <your API token>" \
-H "Accept: application/json"

A token that does not admit the route answers 403. Three shared pages cover behaviour these endpoints inherit rather than define: Errors, Pagination and Throttle limits.

A report is Zentail’s name for a bulk import or export of product data — the same job as the import/export screen in the UI. Three operations cover it, and the important thing about all three is that a report runs asynchronously. You create one, then poll it.

POST /v1/report creates either an import or an export, and isExport decides which. The two directions take different fields, which is why the reference marks each one For imports only: or For exports only:.

An import carries the file itself, base64-encoded in data:

Terminal window
curl -X POST "https://api.zentail.com/v1/report" \
-H "Authorization: <your API token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"label": "MSRP update",
"isExport": false,
"data": "U0tVLE1TUlAKVEVTVFNLVTEsMTkuOTkK",
"extension": "csv",
"overwriteWithBlank": false,
"createNewProducts": false,
"insertOnly": false
}'

The first row of that file must be a header row naming each column. The accepted column headers are listed in the Zentail data dictionary. extension is one of csv, xls, xlsx, xlsm, txt or tsv.

An export names the columns instead, and takes no file:

Terminal window
curl -X POST "https://api.zentail.com/v1/report" \
-H "Authorization: <your API token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"label": "Active SKUs and MSRP",
"isExport": true,
"columnTypes": ["SKU", "MSRP"],
"activeOnly": true,
"includeHeader": true,
"exportForExcel": false,
"headersOnly": false,
"delimiter": ","
}'

Leaving columnTypes out exports every available column, which is a great many of them. filter restricts which products an export covers; the help centre covers how to write a filter formula.

An export returns a Report straight away, before any work has happened:

{
"reportId": 884210,
"label": "Active SKUs and MSRP",
"isExport": true,
"requestedTs": "2026-09-22T09:41:07-04:00",
"completedTs": null,
"status": "PENDING",
"columnTypes": ["SKU", "MSRP"],
"totalLines": null,
"linesSuccessfullyProcessed": null,
"filePath": null,
"errorMessage": null,
"uploader": null
}

Keep reportId. It is how you poll, and it is the only handle you get.

GET /v1/report/{reportId} returns the same object with a current status. When it reaches COMPLETE, filePath holds the finished file and linesSuccessfullyProcessed out of totalLines says how much of an import landed.

Terminal window
curl -X GET "https://api.zentail.com/v1/report/884210" \
-H "Authorization: <your API token>" \
-H "Accept: application/json"

filePath is a signed download URL that expires after 24 hours, not a stored path. It is regenerated every time you read the report, so download it when you get it, and fetch the report again rather than keeping the URL.

GET /v1/report returns reports newest first, wrapped in results with a pagination object.

Terminal window
curl -X GET "https://api.zentail.com/v1/report?createdTs=2026-09-01T00:00:00-04:00&pageLength=25" \
-H "Authorization: <your API token>" \
-H "Accept: application/json"

createdTs returns only reports created after that time, and nextToken is a whole URL rather than a value to append — see Pagination. Sending nextToken makes every other parameter be ignored.

This list is every report on the account, including ones people ran in the UI. The uploader field is how you tell them apart: a null uploader means the report came from the API.

4. Validate a CSV import before you run it

Section titled “4. Validate a CSV import before you run it”

Validation is a dry run. It reads a file, reports what it found and what failed, and changes nothing — so it is the safe way to check a file before spending an import on it.

POST /v1/import/validation/{importDefinitionId} queues one. The path parameter says what kind of import the file is meant for, in three colon-joined parts — <type>:<integration ID>:<template ID> — with the parts that do not apply left empty. TemplatelessProduct::, TemplatedProduct::35, FullProductData:4: and Kits:: are all valid forms.

Send the CSV as the raw request body — not JSON, and not base64. This is the one operation on this page whose request body the reference does not document, so it is worth spelling out:

Terminal window
curl -X POST "https://api.zentail.com/v1/import/validation/TemplatelessProduct::" \
-H "Authorization: <your API token>" \
-H "Content-Type: text/csv" \
--data-binary @products.csv

The response comes back before validation has run, and confirms the file parsed:

{
"import_validation_id": 5512,
"supplied_headers": ["SKU", "MSRP"],
"actual_headers": ["SKU", "MSRP"],
"first_10_rows": [
["TESTSKU1", "19.99"],
["TESTSKU2", "24.99"]
]
}

Check supplied_headers against actual_headers here. A header Zentail did not recognise is the most common reason an import does nothing useful, and this is where it is cheapest to catch.

Then read the results with GET /v1/import/validation/results/{validationID}:

Terminal window
curl -X GET "https://api.zentail.com/v1/import/validation/results/5512" \
-H "Authorization: <your API token>" \
-H "Accept: application/json"
{
"status": "complete",
"data_rows_processed": 2,
"previews": [
{ "name": "MSRP", "message": "2 values will change", "status": "complete" }
],
"validation_result_groups": [
{
"name": "Required fields",
"passed_count": 2,
"failed_count": 0,
"validation_results": [
{
"name": "SKU present",
"message": "Every row has a SKU",
"status": "complete",
"valid_count": 2,
"invalid_count": 0,
"indices": []
}
]
}
]
}

GET /v1/simpleInventory/{SKU} answers one question: how many of this SKU are available to sell, added up across every warehouse.

Terminal window
curl -X GET "https://api.zentail.com/v1/simpleInventory/TESTSKU1" \
-H "Authorization: <your API token>" \
-H "Accept: application/json"
{
"SKU": "TESTSKU1",
"active": true,
"standardProductId": null,
"standardProductIdType": "UPC",
"totalAvailableQuantity": 23,
"lastInventoryUpdateTs": "2026-09-22T09:24:24-04:00"
}

A SKU that does not exist in Zentail answers 404.

6. Look up custom order integrations and vendors

Section titled “6. Look up custom order integrations and vendors”

Two standalone lookups. Neither feeds the operations above — each just returns an identifier you use elsewhere.

GET /v1/customStores lists the integrations orders can be uploaded into — by API, file upload or the UI:

{
"results": [
{ "name": "Wholesale", "integrationId": 51, "default": true },
{ "name": "Trade show", "integrationId": 64, "default": false }
]
}

integrationId is the value to send as integrationID when you create a custom sales order with POST /v1/salesOrder. Your token can call that endpoint, but it is not one of this profile’s eight operations, so it is not in the Seller reference. The integration flagged default is the one Zentail uses when you send no integrationID at all.

GET /v1/vendor/{id} returns one vendor by its Zentail ID — name, lead time, payment window, notes and address. The vendor is wrapped in a results object, even though only one comes back:

{
"results": {
"vendor_id": 118,
"name": "Acme Supply Co",
"lead_time": 14,
"payment_window": 30,
"notes": "Ships Mondays",
"address": {
"name": "Acme Supply Co",
"company": "Acme Supply Co",
"line1": "1 Industrial Way",
"line2": null,
"city": "Trenton",
"state": "NJ",
"postal_code": "08601",
"phone": "555-0100",
"email": "[email protected]",
"country_code": "US"
}
}
}

So read results.vendor_id, not vendor_id. Note too that the field names here are snake_case, unlike the camelCase used by the report and inventory operations above.

API changelog · Built 0c509dd3