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.
What you’ll build
Section titled “What you’ll build”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.
Before you start
Section titled “Before you start”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.
1. Generate an API token
Section titled “1. Generate an API token”In Zentail:
- Go to Account Settings.
- Open the API Access tab.
- Click GENERATE NEW TOKEN.
- 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.
2. Send the token
Section titled “2. Send the token”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.
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.
3. Run an import or export
Section titled “3. Run an import or export”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.
Create a report
Section titled “Create a report”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:
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:
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.
Poll until it finishes
Section titled “Poll until it finishes”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.
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.
List your reports
Section titled “List your reports”GET /v1/report
returns reports newest first, wrapped in results with a pagination object.
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:
curl -X POST "https://api.zentail.com/v1/import/validation/TemplatelessProduct::" \ -H "Authorization: <your API token>" \ -H "Content-Type: text/csv" \ --data-binary @products.csvThe 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}:
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": [] } ] } ]}5. Look up stock for a SKU
Section titled “5. Look up stock for a SKU”GET /v1/simpleInventory/{SKU}
answers one question: how many of this SKU are available to sell, added up across every
warehouse.
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", "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.
See also
Section titled “See also”API changelog · Built 0c509dd3