Flow · 9 steps
Bulk import from a spreadsheet
Bring a spreadsheet of existing data in: upload, preview, map the columns, apply.
Each row becomes the same call a single API request would make, so it is checked, recorded and retried the same way.
Services involved
A filled dot is a read; a ring is a call that changes something.
- Import / Export:
https://import.getmanifold.io
Every call carries Authorization: Bearer $MANIFOLD_TOKEN unless its step says otherwise
(getting a token). Run a flow in your sandbox, not your live
organization: see Before you start.
1 See what can be imported
Each target's field descriptors — name, type, whether it is required, accepted values. The service validates against this same definition and the admin UI builds its mapping form from it, so the two cannot disagree.
GET /targets Import / Export
What the answer holds: the operation's example.
2 Upload the file
The raw CSV as the body. It is parsed and staged as it arrives, so file size is bounded by disk rather than memory. Returns the staged job with its detected headers and row count — nothing has reached a domain service yet.
POST /imports?target=inventory.item&filename=opening-stock.csv Import / Export
sku,name,on hand
WIDGET-BLUE,Blue widget 40mm,40
WIDGET-RED,Red widget 40mm,25
WIDGET-GREEN,Green widget 40mm,0Answers 202. What the answer holds: the operation's example.
From your answer, later steps send id as {jobId}.
Records import.job-created.
3 Preview it
The first rows as parsed, plus a mapping suggested by matching the file's headers against the target's field names and aliases. The suggestion is a starting point — it is never applied on its own.
GET /imports/{jobId}/preview?limit=5 Import / Export
What the answer holds: the operation's example.
4 Confirm the mapping
Which column feeds which field. Columns are addressed by index because CSV headers are not reliably unique. Rejected if a field is unknown, two columns claim the same field, or a required field is unmapped — and re-mapping a staged job simply replaces the mapping. No location column here: unmapped means the organization's own location, which is what a single-site merchant wants and what most importers are.
PUT /imports/{jobId}/mapping Import / Export
{
"hasHeaderRow": true,
"columns": [
{
"column": 0,
"field": "sku"
},
{
"column": 1,
"field": "name"
},
{
"column": 2,
"field": "quantity"
}
]
}What the answer holds: the operation's example.
Records import.mapped.
5 Run it
Queued, not performed — and the distinction is visible in the journal: this call appends nothing. The response is the job id, the work goes onto a JetStream queue, and import.started is appended by the worker when it picks the job up. Progress then arrives on the organization's event stream, which is why there is nothing to poll.
POST /imports/{jobId}/commit Import / Export
Answers 202. What the answer holds: the operation's example.
6 Watch it finish
Live counters: applied + failed + skipped reaches rows when the job is done. Skipped covers the header row and any row a replay found already applied — which is what makes a re-run of a half-finished import safe. import.progressed is throttled to every N rows or M milliseconds, so a small file like this one goes from started straight to completed without a single progress event: one event per row would put a million of them in the journal for a million-row import, which is the case the throttle exists for.
GET /imports/{jobId} Import / Export
What the answer holds: the operation's example.
Records import.started, import.completed.
7 Read the rejects
Every row the import refused, with the field and the reason — enough to correct the file and upload it again. One bad row never aborts the job.
GET /imports/{jobId}/failures Import / Export
What the answer holds: the operation's example.
8 Hand rows over as JSON
The same pipeline without a file: what a service that already holds structured data — a channel app's catalog pull, a migration from another system — sends. headers are the target's field names and rows the cells in that order; the rows are staged as an upload's would be and the identity mapping validated the same way, so a submitted batch is applied, checked and reported exactly as a file is. reference is the batch's key: a second submission with it answers with the job already staged. Here run is false, so the job waits for a commit like the uploaded one did.
POST /imports/rows Import / Export
{
"reference": "mig_01a15889-f997-7a8f-9d1c-2e5b8a7f6c30:locations:1",
"target": "inventory.location",
"headers": [
"name",
"fulfillable",
"note"
],
"rows": [
[
"Outlet",
true,
"Shopfront"
],
[
"Quarantine",
false,
"Bay 4"
]
],
"run": false
}Answers 202. What the answer holds: the operation's example.
Records import.job-created, import.mapped.
9 Export it back out
The same targets in reverse. Asking is a command — the export is keyed by the caller's exportId and appends import.export-requested — and the answer is a link, signed by the service and good for five minutes, that a browser follows without a bearer to stream the CSV out of the domain services rather than materialised whole. The download is a read and appends nothing. Accounting is export-only: it owns no commands.
POST /exports Import / Export
{
"exportId": "exp_01a15865-aaa4-7d6c-9e8f-1a2b3c4d5e6f",
"target": "inventory.location",
"columns": [
"name",
"fulfillable"
]
}What the answer holds: the operation's example.
Records import.export-requested.
What a recorded fact is: Facts and the journal. Why a repeated step changes nothing: Idempotency and retries.