Skip to content

Imports API

POST /api/v1/imports/{kind} loads a whole file of customers or transactions in one request. It is what Settings → Data import uses; this page is for scripting it and for checking exactly what it returns. For a step-by-step guide, the column tables and fixes for common errors, see Importing a file.

Each row is checked against the same rules as the ingest API and loaded through the same idempotent path, so importing a file twice only reports duplicates.

Who can call it. A signed-in person with settings.manage (by default the Administrator). An API key gets 401 sign in required; a person without the permission gets 403 missing permission settings.manage. Your systems should send data through the ingest API with an API key instead; this endpoint is for people loading files.

Signing in with curl. Sign in once and keep the session cookie in a file, as described in Alerts and cases API: before you start, then add -b ~/.sp-cookies to the requests below.

Where it runs. Imports are served by processes running the api role. If none of them can reach Kafka and the database, the request fails with 503 imports need the ingest service on this node.

Permission settings.manage
Path kind is transactions or customers
Body The file itself: CSV with a header row (Content-Type: text/csv), or JSON (application/json) as an array, {"items": [...]} or one object per line
Query format: csv or json. Optional; when omitted, a body starting with [ or { is JSON and anything else is CSV
Limits 50,000 rows; SP_INGEST_MAX_BODY_BYTES (8 MB by default). Imports get their own deadlines, 5 minutes to read the request and 10 minutes to write the response, instead of SP_HTTP_*_TIMEOUT; a reverse proxy in front needs a body limit and timeouts at least that large
Times CSV times without Z or an offset are read in the workspace’s time zone; JSON times must be RFC 3339 with a zone

The CSV columns are listed in Importing a file: the columns. JSON records use the field names of the ingest API (transactions, customers).

The request succeeds even when some rows are rejected; check the counts.

{
"rows": 1200,
"accepted": 1187,
"duplicate": 10,
"stale": 0,
"rejected": 3,
"retry": 0,
"errors": [
{ "line": 14, "message": "amount \"1.234\" has more decimals than GHS allows" },
{ "line": 233, "message": "expected value to be one of \"in, out, internal\" (direction: debit)" },
{ "line": 871, "message": "no FX rate for USD→GHS on or before 2026-09-30; load rates via /api/v1/ingest/fx-rates" }
]
}
Field Meaning
rows Data rows in the file, counting the ones that couldn’t be read
accepted New records stored
duplicate Records already held with exactly the same content. Nothing changed
stale Customer records whose version is not newer than the one held (JSON only; CSV has no version column). Nothing changed. See Versions
rejected Rows that failed a check
retry Rows not stored because of a temporary problem. Send the file again
errors Up to 200 problems as {line, message}. Problems from every stage (reading, validation, loading) are sorted by line, then cut to the first 200. In CSV, line 1 is the header. In newline-delimited JSON, line is the physical line in the file; in a JSON array or {"items": …}, it is the item’s position from 1

The statuses mean exactly what they mean in the ingest API; see Responses and statuses.

Status When Example detail
401 Not signed in, or an API key sign in required
403 Missing settings.manage missing permission settings.manage
413 More than 50,000 rows, or a body over the size limit 60000 rows; split the file into parts of up to 50000
422 The file can’t be read at all: no header, missing required columns, or no data rows missing column(s): occurred_at, currency
503 This server can’t load data imports need the ingest service on this node

A body over the size limit is refused before SpeculaPro reads it, with a plain-text request body too large rather than a JSON problem.

Customers from a CSV file:

Terminal window
curl -s -b ~/.sp-cookies -H 'Content-Type: text/csv' \
--data-binary @customers.csv \
"$SP/api/v1/imports/customers"

Transactions from a newline-delimited JSON export:

Terminal window
curl -s -b ~/.sp-cookies -H 'Content-Type: application/json' \
--data-binary @transactions.ndjson \
"$SP/api/v1/imports/transactions"

Use --data-binary, not -d: -d removes the line breaks, which turns a CSV or one-per-line file into a single line.