Imports
A mass import turns a spreadsheet into passports in bulk. Upload a file, preview it to build a column mapping, start the import, then poll it, inspect the rows that failed, and retry them with corrections. One mass import is far kinder to rate limits than firing thousands of single-passport creates.
An import belongs to the ontology of the passport it runs under, so imports:read and imports:write sit inside ontologies:read and can be restricted to specific ontologies.
The import model
POST /v1/imports and GET /v1/imports/{import} return the full object below. GET /v1/imports returns a reduced shape.
- Name
object- Type
- string
- Description
Always
import.
- Name
id- Type
- string
- Description
Unique identifier, e.g.
import_8c2d5e1f9a4b7016.
- Name
target- Type
- string
- Description
The passport the import runs under — usually the ontology's root passport. Nullable.
- Name
status- Type
- string
- Description
pendinguntil a coordinator picks the run up. While it enumerates the file, the status names the long-running step:downloading(fetching the workbook),counting(sizing every sheet of a multi-level import),preparing(building a level's parent map) anddispatching(enqueueing row workers).importingwhile workers process rows, thencomplete,complete_with_errors(some rows failed but the run finished) orfailed— the three terminal states. A multi-level import passes throughpreparinganddispatchingagain for each later level.
- Name
file- Type
- string
- Description
The uploaded file the rows are read from. Nullable.
- Name
levels- Type
- array
- Description
The mapping the import was started with — one entry per level in worksheet order, each pairing a
levelkey with its columnmapping. Nullable.
- Name
total_rows- Type
- integer
- Description
Rows the import expects to process, summed across levels. Compare it against what you expected: a workbook short of sheets contributes zero for every level past the last one.
- Name
dispatched_rows- Type
- integer
- Description
Row workers enqueued so far by the current enumeration pass. It advances while the status is
dispatching(updated every ~1,000 rows) and is the only counter that moves before workers start finishing.
- Name
processed_rows- Type
- integer
- Description
Rows that produced a passport. Failures are counted separately, so progress against
total_rowsisprocessed_rows + failed_rows.
- Name
failed_rows- Type
- integer
- Description
Rows that failed for any reason — always the sum of the two counters below. A successful retry decrements it.
- Name
failed_validation_rows- Type
- integer
- Description
Rows the ontology rejected on their content. Fix the cells and retry those rows individually.
- Name
failed_infra_rows- Type
- integer
- Description
Rows that failed on storage or transport rather than on their content — the kind worth re-running wholesale with
scope: "infra_all".
- Name
phases- Type
- array
- Description
Live per-level progress for a multi-level import: one entry per
levelsentry, each carrying the level'sdepth, its rowtotal, the rows accounted for so far (done, processed plus failed) and itsstatus—pending,runningorcomplete. Empty on a single-level import, and until a multi-level import has counted its sheets.
- Name
metadata- Type
- object
- Description
Caller-owned metadata bag, set at create time. Imports are immutable once started, so there is no update path.
- Name
started_at- Type
- string
- Description
ISO 8601 timestamp of when the import was started.
- Name
finished_at- Type
- string
- Description
ISO 8601 timestamp of when the import reached a terminal status.
nullwhile it is still in flight.
The import row model
- Name
object- Type
- string
- Description
Always
import_row.
- Name
id- Type
- string
- Description
Row id, shaped
"{phase}#{zero-padded index}"— e.g."0#000017". This is the id you quote back on retry.
- Name
status- Type
- string
- Description
doneonce the row's passport was written;failed_validationwhen the ontology rejected its content;failed_infrawhen storage or transport failed, which is the case worth retrying unchanged;retryingwhile a retry is in flight.
- Name
row- Type
- array
- Description
The raw spreadsheet cells.
- Name
errors- Type
- array
- Description
Why the row failed — for
failed_validation, one entry per rejected property with itsproperty_id,property_name,code,messageand therawcell; forfailed_infra, the infrastructure failure.nullon a row that succeeded.
- Name
attempts- Type
- integer
- Description
How many times the row has been processed, counting retries.
0until the first attempt is recorded.
- Name
passport- Type
- string
- Description
The
pass_…id created from this row oncedone, otherwisenull.
Start a mass import
Reads rows rows from an uploaded file, maps each spreadsheet column to an ontology property, and creates a passport per row under target. Build the mapping from the file preview, then poll the returned import until its status is terminal. Returns 201 with the import in pending. Requires imports:write. Accepts an optional Idempotency-Key header.
Required attributes
- Name
file- Type
- string
- Description
The uploaded file's id (a
file_…id). xlsx, csv and ods are importable.
- Name
target- Type
- string
- Description
The passport to import under — usually the ontology's root passport (a
pass_…id).
- Name
rows- Type
- integer
- Description
Data row count from the file preview. Minimum 1, and never more than the file's size in bytes — a larger count is a stale-preview mistake and is rejected. It seeds
total_rowsuntil the import counts the file itself.
- Name
levels- Type
- array
- Description
One to 10 entries, one per level the import creates passports at. Each pairs a
levelkey (case-insensitive, at most 4 characters) with amappingof column index → property id, the literalfriendly_id, ornone. Mapping keys are 0-based column indexes given as strings; unmapped columns are ignored.
Optional attributes
- Name
metadata- Type
- object
- Description
A metadata bag, set here and only here.
What a column can feed
Only properties that hold a value import: short_text, long_text, number, boolean, date, tags, range, measurement, file and image. A column mapped to a category divider or a computed composite is ignored.
A mapped column writes the property in the ontology's default language. Translatable properties (short_text, long_text, tags and file) get their other languages through PATCH /v1/passports/{passport} once the passports exist; a mapping target is a bare property id, so property_capacity@de is refused as an unknown property.
Multi-level imports read one worksheet per level
With more than one levels entry the import runs in phases, and phase N reads worksheet N — levels[0] from the first sheet, levels[1] from the second, and so on. The worksheet is fixed by the level's position and cannot be chosen. Two consequences:
- A workbook with fewer sheets than
levelsentries imports zero rows for every level past the last sheet, and the import still finishescomplete. Checktotal_rowsagainst what you expected. - Column 0 is special regardless of
mapping: on the parent level it is the passport'sfriendly_id, and on every child sheet it is the reference to the parent row.
The file preview lists sheets in workbook order — use it to confirm the order matches your levels.
The request is validated up front, before any row is processed, so a bad
mapping fails fast with 400 parameter_invalid (param naming the
offending field) instead of failing every row at import time: every
level must exist in the ontology, every mapped column must target a
property that level actually defines (friendly_id and none aside), and
rows cannot exceed the file's byte size. An import is a creation
surface, so this resolves against the ontology's latest version, and
the passports it creates pin that version. Re-derive rows and the
mapping from the file preview if either
is rejected.
A team without a company — a supplier team, onboarded through a supplier
invitation — cannot issue passports, so this endpoint refuses it with
403 company_required before anything is read. Everything else the
key was granted stays available.
Request
curl https://api.synexcloud.com/v1/imports \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-H "Idempotency-Key: packs-2026-07-22" \
-H "Content-Type: application/json" \
-d '{
"file": "file_00a1b2c3d4e5f607",
"target": "pass_3e9a1c5b7d2f4083",
"rows": 1240,
"levels": [
{
"level": "B",
"mapping": { "0": "friendly_id", "1": "property_capacity", "2": "property_chemistry" }
},
{
"level": "C",
"mapping": { "0": "friendly_id", "1": "property_cell_serial", "2": "property_capacity" }
}
],
"metadata": { "source_system": "erp-prod" }
}'
Response
{
"object": "import",
"id": "import_8c2d5e1f9a4b7016",
"target": "pass_3e9a1c5b7d2f4083",
"status": "pending",
"file": "file_00a1b2c3d4e5f607",
"levels": [
{
"level": "B",
"mapping": { "0": "friendly_id", "1": "property_capacity", "2": "property_chemistry" }
},
{
"level": "C",
"mapping": { "0": "friendly_id", "1": "property_cell_serial", "2": "property_capacity" }
}
],
"total_rows": 1240,
"dispatched_rows": 0,
"processed_rows": 0,
"failed_rows": 0,
"failed_validation_rows": 0,
"failed_infra_rows": 0,
"phases": [],
"metadata": { "source_system": "erp-prod" },
"started_at": "2026-07-22T14:10:43Z",
"finished_at": null
}
List imports
Lists the team's imports, latest first. Returns a paginated list. Requires imports:read; a key whose imports:read is restricted to specific ontologies cannot use this team-wide list (403 ability_not_scoped_to_resource) — fetch imports by id instead.
List rows come from an index that carries neither the mapping nor the per-failure counters, so each row is a reduced shape: object, id, target, status, total_rows, processed_rows, metadata and created_at — the one place an import reports when it was created. Fetch by id for the full object.
Optional attributes
- Name
created[gte]- Type
- string
- Description
Only imports created at or after this time. Unix seconds or an ISO date, inclusive of the whole second named.
- Name
created[lte]- Type
- string
- Description
Only imports created at or before this time. An inverted range is
400.
- Name
limit- Type
- integer
- Description
Page size, 1–100 (default 10).
- Name
cursor- Type
- string
- Description
A pagination cursor from a previous response's
next_cursor.
The creation window runs as a SQL WHERE rather than a scan, so pages stay exact. Filters join the cursor's scope; see pagination.
Every row here reports metadata: {} whatever the import actually
carries — the index has no metadata column. GET /v1/imports/{import}
is truthful; fetch by id when you need the real bag, and do not
reconcile against the list.
Request
curl -G https://api.synexcloud.com/v1/imports \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-d limit=10
Response
{
"object": "list",
"data": [
{
"object": "import",
"id": "import_8c2d5e1f9a4b7016",
"target": "pass_3e9a1c5b7d2f4083",
"status": "complete_with_errors",
"total_rows": 16120,
"processed_rows": 16120,
"metadata": {},
"created_at": "2026-07-22T14:10:43Z"
},
{
"object": "import",
"id": "import_5b0e9d3a7c1f4682",
"target": "pass_3e9a1c5b7d2f4083",
"status": "complete",
"total_rows": 480,
"processed_rows": 480,
"metadata": {},
"created_at": "2026-07-15T09:02:11Z"
}
],
"has_more": true,
"next_cursor": "WyJpbXBvcnRfOGMyZDVlMWY5YTRiNzAxNiJd",
"url": "/v1/imports"
}
Retrieve an import
Retrieves one import by id, with live progress counters. Poll it until status is complete, complete_with_errors, or failed. Requires imports:read.
Before workers start finishing, dispatched_rows is the counter that moves; after that, processed_rows + failed_rows against total_rows is the progress, and phases breaks it down per level.
Request
curl https://api.synexcloud.com/v1/imports/import_8c2d5e1f9a4b7016 \
-H "Authorization: Bearer $SYNEX_API_KEY"
Response
{
"object": "import",
"id": "import_8c2d5e1f9a4b7016",
"target": "pass_3e9a1c5b7d2f4083",
"status": "importing",
"file": "file_00a1b2c3d4e5f607",
"levels": [
{
"level": "B",
"mapping": { "0": "friendly_id", "1": "property_capacity", "2": "property_chemistry" }
},
{
"level": "C",
"mapping": { "0": "friendly_id", "1": "property_cell_serial", "2": "property_capacity" }
}
],
"total_rows": 16120,
"dispatched_rows": 1240,
"processed_rows": 1195,
"failed_rows": 12,
"failed_validation_rows": 9,
"failed_infra_rows": 3,
"phases": [
{ "depth": "B", "total": 1240, "done": 1207, "status": "running" },
{ "depth": "C", "total": 14880, "done": 0, "status": "pending" }
],
"metadata": { "source_system": "erp-prod" },
"started_at": "2026-07-22T14:10:43Z",
"finished_at": null
}
List an import's rows
Lists an import's per-row outcomes, so you can see exactly which rows failed and why. Filter with status; passing status=failed selects both validation and infrastructure failures. Requires imports:read.
Pages hold a fixed 100 rows and this endpoint takes no limit — page with cursor until has_more is false. Beside the list envelope the response carries header — one {index, label, property_id} entry per column, property_id being null on an unmapped one — and the mapping, for rendering rows back to their columns.
Optional attributes
- Name
status- Type
- string
- Description
Filter by outcome —
failed(both failure kinds),done,failed_validation,failed_infra, orretrying.
- Name
cursor- Type
- string
- Description
A pagination cursor from a previous response's
next_cursor.
Request
curl -G https://api.synexcloud.com/v1/imports/import_8c2d5e1f9a4b7016/rows \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-d status=failed
Response
{
"object": "list",
"data": [
{
"object": "import_row",
"id": "0#000017",
"status": "failed_validation",
"row": ["PACK-0018", "seventy-eight", "NMC-811"],
"errors": [
{
"property_id": "property_capacity",
"property_name": "Capacity",
"code": "invalid_measurement",
"message": "Expected a numeric measurement.",
"raw": "seventy-eight"
}
],
"attempts": 1,
"passport": null
}
],
"has_more": true,
"next_cursor": "WyJpbXBvcnRfOGMyZDVlMWY5YTRiNzAxNiJd",
"url": "/v1/imports/import_8c2d5e1f9a4b7016/rows",
"header": [
{ "index": 0, "label": "Friendly ID", "property_id": "friendly_id" },
{ "index": 1, "label": "Capacity", "property_id": "property_capacity" },
{ "index": 2, "label": "Chemistry", "property_id": "property_chemistry" }
],
"mapping": {
"0": "friendly_id",
"1": "property_capacity",
"2": "property_chemistry"
}
}
Export failed rows
Returns the failed rows as text/csv, not JSON — the 200 body is the file itself, not an envelope. Requires imports:read. It is served as an attachment named {import_id}_failed_rows.csv.
The file holds both kinds of failure — rows rejected by validation and rows lost to an infrastructure error — one row each, laid out in the import's own column order, plus a trailing Errors column holding that row's failures as Property: message fragments joined by ; . You get the original cells and the reason side by side, which is the whole point: the sheet is repairable in place.
An import with no failed rows returns a header-only file — not a 404, and not an empty body. A consumer can treat "one line" as "nothing to fix" without special-casing an error status.
The byte-order mark
The file is UTF-8 with a BOM, deliberately. It exists to be opened in a spreadsheet, and Excel reads a BOM-less UTF-8 CSV as the local 8-bit codepage, mangling every non-ASCII character in it — an ontology with a °C unit or a supplier name carrying an accent comes out as mojibake for the very people the export is for.
The cost lands on programmatic consumers instead: strip the BOM before reading the first header cell, or your first column will not match by name. It is the single most common cause of a header lookup that mysteriously fails on column one only.
When to use it
If you are handling failures programmatically, use GET /v1/imports/{import}/rows instead — it returns the same rows as JSON, with structured per-property errors, no BOM and no CSV parsing in the way.
This endpoint exists because in practice the people who repair import failures work in a spreadsheet. Export the failures, fix the cells where they already live, then feed the corrections back through retry — the retry payload takes column index → corrected value, which maps directly onto the columns you just edited.
Request
curl https://api.synexcloud.com/v1/imports/import_8c2d5e1f9a4b7016/rows/export \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-o failed_rows.csv
# Strip the BOM on the way in
sed '1s/^\xEF\xBB\xBF//' failed_rows.csv > clean.csv
Response
Friendly ID,Capacity,Chemistry,Errors
PACK-0018,seventy-eight,NMC-811,Capacity: Expected a numeric measurement.
PACK-0032,"80,9",NMC-811,Capacity: Expected a numeric measurement.
Retry failed rows
Retries an import's failed rows, optionally with inline cell corrections. Supply rows — each entry pairs a row_id with a corrected_row that maps a column index to a fixed value — and/or scope: "infra_all" to re-run every infrastructure failure without listing them. Send at least one of the two; an empty body is 400 parameter_invalid. Requires imports:write. Accepts an optional Idempotency-Key header.
Corrections are re-validated server-side first. The response sorts every row you named into one of three lists:
dispatched— re-queued; the row moves toretrying.still_invalid— failed re-validation, returned with itserrorsand not dispatched. A child-level row whose parent never imported comes back here with codeparent_unresolved: retry the parent row first.skipped— no such row, or one that is not in a failed state any more (already retrying, or done).
Optional attributes
- Name
rows- Type
- array
- Description
Rows to retry. Each entry needs a
row_idand may carry acorrected_rowmapping column index → new cell value.
- Name
scope- Type
- string
- Description
Set to
infra_allto retry every infrastructure failure at once.
Request
curl https://api.synexcloud.com/v1/imports/import_8c2d5e1f9a4b7016/retry \
-H "Authorization: Bearer $SYNEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rows": [
{ "row_id": "0#000017", "corrected_row": { "1": 78.4 } },
{ "row_id": "0#000031", "corrected_row": { "1": "80,9" } },
{ "row_id": "0#000055" }
]
}'
Response
{
"object": "import.retry",
"import": "import_8c2d5e1f9a4b7016",
"dispatched": ["0#000017"],
"still_invalid": [
{
"row_id": "0#000031",
"errors": [
{
"property_id": "property_capacity",
"property_name": "Capacity",
"code": "invalid_measurement",
"message": "Expected a numeric measurement.",
"raw": "80,9"
}
]
}
],
"skipped": ["0#000055"]
}