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

    pending until 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) and dispatching (enqueueing row workers). importing while workers process rows, then complete, complete_with_errors (some rows failed but the run finished) or failed — the three terminal states. A multi-level import passes through preparing and dispatching again 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 level key with its column mapping. 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_rows is processed_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 levels entry, each carrying the level's depth, its row total, the rows accounted for so far (done, processed plus failed) and its status — pending, running or complete. 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. null while 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

    done once the row's passport was written; failed_validation when the ontology rejected its content; failed_infra when storage or transport failed, which is the case worth retrying unchanged; retrying while 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 its property_id, property_name, code, message and the raw cell; for failed_infra, the infrastructure failure. null on a row that succeeded.

  • Name
    attempts
    Type
    integer
    Description

    How many times the row has been processed, counting retries. 0 until the first attempt is recorded.

  • Name
    passport
    Type
    string
    Description

    The pass_… id created from this row once done, otherwise null.


POST/v1/imports

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_rows until 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 level key (case-insensitive, at most 4 characters) with a mapping of column index → property id, the literal friendly_id, or none. 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 levels entries imports zero rows for every level past the last sheet, and the import still finishes complete. Check total_rows against what you expected.
  • Column 0 is special regardless of mapping: on the parent level it is the passport's friendly_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.

Request

POST
/v1/imports
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
}

GET/v1/imports

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.

Request

GET
/v1/imports
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"
}

GET/v1/imports/:id

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

GET
/v1/imports/import_8c2d5e1f9a4b7016
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
}

GET/v1/imports/:id/rows

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, or retrying.

  • Name
    cursor
    Type
    string
    Description

    A pagination cursor from a previous response's next_cursor.

Request

GET
/v1/imports/import_8c2d5e1f9a4b7016/rows
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"
  }
}

GET/v1/imports/:id/rows/export

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

GET
/v1/imports/import_8c2d5e1f9a4b7016/rows/export
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.

POST/v1/imports/:id/retry

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 to retrying.
  • still_invalid — failed re-validation, returned with its errors and not dispatched. A child-level row whose parent never imported comes back here with code parent_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_id and may carry a corrected_row mapping column index → new cell value.

  • Name
    scope
    Type
    string
    Description

    Set to infra_all to retry every infrastructure failure at once.

Request

POST
/v1/imports/import_8c2d5e1f9a4b7016/retry
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"]
}

Was this page helpful?