Skip to content

Imports

The Imports API is the async, streaming alternative to POST /products. You hand Humind a pre-signed upload URL, push an NDJSON file straight to blob storage, and Humind ingests the file in the background using the same validation and upsert pipeline as the synchronous endpoints.

Use it when you have a large catalog to push (initial backfill, daily mirror of your ERP, full re-sync after a schema change) and the synchronous batch endpoint isn't a good fit anymore. There's no per-item HTTP round-trip, you upload once, start the import once, and poll for status.

Choose your endpoint

Catalog sizeFrequencyRecommended endpoint
1 productOne-off or webhook-drivenPOST /products
Up to 500 productsOccasional bulk updatePOST /products/batch
500+ products, or any size on a recurring scheduleDaily or weekly full syncPOST /imports (this page)

The Imports API and the synchronous endpoints write to the same catalog, they're not separate stores. You can mix and match: backfill with an import, then keep the catalog fresh with POST /products calls from your webhook handler.

Workflow overview

The full import lifecycle has four steps. The merchant talks to the Humind API for steps 1, 3, and 4; step 2 is a direct upload to Azure Blob Storage using the URL Humind returned in step 1.

1. POST /imports                       → { sync_id, upload_url }
2. PUT  <upload_url>  (NDJSON body)    → 201 from Azure Blob
3. POST /imports/{sync_id}/start       → { status: "processing" }
4. GET  /imports/{sync_id}             → poll until status="done" or "failed"

A fifth optional step, POST /imports/{sync_id}/cancel, stops a running import at the next checkpoint.

Required scope for every step: imports:write. The same scope covers reads.


Step 1: Create the import

POST /imports reserves a sync_id and returns a short-lived pre-signed URL you can PUT your NDJSON file to.

Required scope: imports:write

Request

bash
curl -X POST https://api.thehumind.com/public/v1/imports \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 3f1a8d92-7c4b-4e6f-b2a1-d5e9c8f7a3b4" \
  -H "Content-Type: application/json" \
  -d '{
    "resource_type": "product",
    "format": "ndjson"
  }'
FieldTypeRequiredDescription
resource_type'product'YesWhat kind of resource the file contains. Only product is supported today.
format'ndjson'YesThe on-the-wire format. Only ndjson (newline-delimited JSON) is supported today.

Response 201 Created

json
{
  "sync_id": "b5f9e3a2-7c1d-4e8a-9b3f-0c2d4e5f6a7b",
  "status": "pending",
  "upload_url": "https://<storage>.blob.core.windows.net/<container>/b5f9e3a2-7c1d-4e8a-9b3f-0c2d4e5f6a7b.ndjson?sv=2024-...&sig=...",
  "expires_at": "2026-04-25T15:30:00Z"
}
FieldTypeDescription
sync_idstringThe handle for this import. Use it in the path of every subsequent call.
statusstringAlways pending at this point, the file hasn't been uploaded yet.
upload_urlstringThe pre-signed URL you PUT your NDJSON to. Scoped to write/create on this single blob; can't be used to read or list anything else.
expires_atISO 8601When the upload_url stops accepting writes. One hour from creation.

The upload URL expires in one hour

If you don't PUT your file before expires_at, the URL becomes useless and you'll need to call POST /imports again to get a fresh one. The sync_id is cheap to recreate, don't try to reuse a stale one.


Step 2: Upload your NDJSON

Once you have the upload_url, send your file directly to Azure Blob Storage with a single PUT. No Humind authentication is involved here: the SAS token in the URL is the only credential needed, and it's already scoped to write this one blob.

File format

NDJSON is "newline-delimited JSON": one JSON object per line, separated by \n (not \r\n), no surrounding array, no commas between lines, UTF-8. Each line must be a valid Product payload, the same shape POST /products accepts.

Example products.ndjson:

{"external_id":"SKU-001","title":"Crème hydratante","handle":"creme-hydratante","default_language":"fr","variants":[{"external_id":"SKU-001-50ML","title":"50 ml","price":29.90,"currency":"EUR"}]}
{"external_id":"SKU-002","title":"Sérum éclat","handle":"serum-eclat","default_language":"fr","variants":[{"external_id":"SKU-002-30ML","title":"30 ml","price":49.00,"currency":"EUR"}]}
{"external_id":"SKU-003","title":"Baume lèvres","handle":"baume-levres","default_language":"fr","variants":[{"external_id":"SKU-003-15ML","title":"15 ml","price":12.50,"currency":"EUR"}]}

Request

bash
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/x-ndjson" \
  -H "x-ms-blob-type: BlockBlob" \
  --upload-file products.ndjson

The x-ms-blob-type: BlockBlob header is required by Azure Blob Storage for all PUT uploads. The Content-Type: application/x-ndjson header is recommended so downstream tooling tags the blob correctly.

Response 201 Created

Azure returns 201 Created with no body on success. Any other status (typically 403 for an expired SAS, 400 for a missing required header) means the upload didn't land, fix the cause before calling start.

NDJSON gotchas

  • One product per line, \n-terminated. Don't pretty-print, that breaks the line splitter.
  • No leading [ or trailing ] and no commas between lines. NDJSON is not a JSON array.
  • UTF-8 only. If your dataset has accented characters, save the file as UTF-8 without a BOM.
  • A trailing empty line is fine and ignored. Any line that fails to parse is reported in error_logs after the import runs.

Step 3: Start processing

POST /imports/{sync_id}/start hands the import to the background worker. Humind streams the blob line by line, validates each product against the same schema as POST /products, and upserts it into your catalog.

Required scope: imports:write

Request

bash
curl -X POST https://api.thehumind.com/public/v1/imports/b5f9e3a2-7c1d-4e8a-9b3f-0c2d4e5f6a7b/start \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 7a2b3c4d-5e6f-4a8b-9c0d-1e2f3a4b5c6d"

Response 202 Accepted

json
{
  "status": "processing"
}

The minimal body confirms the import has been queued. Use GET /imports/{sync_id} for the full state including started_at, counters, and error_logs.

The call returns as soon as the import is queued, usually under a second. The actual ingestion runs in the background; track progress with the polling endpoint.

Upload before you start

Calling start before you've successfully PUT the blob returns 422 Unprocessable Entity with code import_blob_missing. Calling start on an import that's already past pending returns 422 with code import_not_pending.


Step 4: Poll status

GET /imports/{sync_id} returns the current state of an import: counts so far, the report breakdown, and the latest error log entries.

Required scope: imports:write

Request

bash
curl https://api.thehumind.com/public/v1/imports/b5f9e3a2-7c1d-4e8a-9b3f-0c2d4e5f6a7b \
  -H "Authorization: Bearer hmd_live_..."

Response 200 OK: while processing

json
{
  "sync_id": "b5f9e3a2-7c1d-4e8a-9b3f-0c2d4e5f6a7b",
  "status": "processing",
  "resource_type": "product",
  "total_products": 12000,
  "synced_products": 4321,
  "report": {
    "created": 3210,
    "updated": 1100,
    "failed": 11
  },
  "error_logs": [
    {
      "message": "Validation failed on line 742: variants[0].price must be a number",
      "product_id": "SKU-0742",
      "timestamp": "2026-04-25T14:32:01Z"
    }
  ],
  "started_at": "2026-04-25T14:31:12Z",
  "completed_at": null
}

Response 200 OK: once done

json
{
  "sync_id": "b5f9e3a2-7c1d-4e8a-9b3f-0c2d4e5f6a7b",
  "status": "done",
  "resource_type": "product",
  "total_products": 12000,
  "synced_products": 12000,
  "report": {
    "created": 9876,
    "updated": 2100,
    "failed": 24
  },
  "error_logs": [
    {
      "message": "Validation failed on line 1042: handle is required",
      "product_id": "SKU-1042",
      "timestamp": "2026-04-25T14:38:14Z"
    }
  ],
  "started_at": "2026-04-25T14:31:12Z",
  "completed_at": "2026-04-25T14:41:55Z"
}

Polling cadence

Once every 5 to 10 seconds is sufficient. The status field only changes at known transitions (pending → processing → done|failed|cancelled), so polling faster doesn't surface anything new.


The Import object

FieldTypeDescription
sync_idstringStable identifier for this import. Use it in every subsequent call.
status'pending' | 'processing' | 'done' | 'failed' | 'cancelled'Lifecycle state. See State machine below.
resource_type'product'What kind of resource the file contained.
total_productsintegerNumber of lines parsed from the NDJSON file. Set after the file is fully read; may be 0 while pending or in the very early seconds of processing.
synced_productsintegerNumber of lines that were successfully created or updated so far. Failed lines (validation errors, malformed JSON) are not counted here, they live in report.failed. When the import is done, synced_products === report.created + report.updated and total_products = synced_products + report.failed.
report.createdintegerNumber of products that didn't exist before and were inserted.
report.updatedintegerNumber of products that already existed (matched on external_id) and were updated in place.
report.failedintegerNumber of lines that failed validation or upsert. This count is exact even if some entries are no longer present in error_logs.
error_logsobject[]The most recent failure entries. Capped at the last 100. See Error log entries.
upload_urlstringReturned only by POST /imports. Not echoed by GET.
expires_atISO 8601Upload URL expiry. Returned only by POST /imports.
started_atISO 8601 | nullWhen start was called. null while pending.
completed_atISO 8601 | nullWhen the import reached done, failed, or cancelled. null until then.
created_atISO 8601When the import was created (call to POST /imports).

State machine

pending  ──start──▶  processing  ──finish──▶  done
   │                      │
   │                      ├──fatal error──▶  failed
   │                      └──cancel──────▶  cancelled
   └──cancel──▶  cancelled

Once an import reaches a terminal state (done, failed, cancelled), it stays there. Re-running an import means creating a new one with POST /imports and uploading a fresh file.


Cancelling an import

POST /imports/{sync_id}/cancel stops an import that's still pending or processing. It's best-effort: a processing import keeps running until the next checkpoint (typically every few hundred lines), at which point the worker notices the cancel flag and stops cleanly.

Required scope: imports:write

Request

bash
curl -X POST https://api.thehumind.com/public/v1/imports/b5f9e3a2-7c1d-4e8a-9b3f-0c2d4e5f6a7b/cancel \
  -H "Authorization: Bearer hmd_live_..." \
  -H "Idempotency-Key: 9b8a7c6d-5e4f-4a3b-2c1d-0e9f8a7b6c5d"

Response 200 OK

json
{
  "status": "cancelled"
}

Use GET /imports/{sync_id} afterward for the full state including completed_at and counters.

Products that were already created or updated before the cancel point stay in your catalog. The cancel doesn't roll back. If you need to undo, push a corrected import or use the synchronous PATCH/DELETE endpoints to fix specific entries.

Cancelling a done, failed, or already cancelled import returns 422 with code import_not_pending.


Error log entries

Each entry in error_logs describes one line from the NDJSON file that didn't make it into the catalog.

FieldTypeDescription
messagestringHuman-readable summary of the failure including the line number, e.g. Validation failed on line 3: Required or Invalid JSON on line 4: Expected property name…. The string is stable enough to grep for the broad class (Validation failed, Invalid JSON) but is not the machine error code. For per-field debug, replay the offending line through POST /products.
product_idstringThe external_id from the offending line, when it could be parsed. Omitted entirely when the line was malformed JSON (the message itself includes the line number, e.g. Invalid JSON on line 1042: …).
timestampISO 8601When the worker saw the failure.

The list is capped at the last 100 failures to keep the response payload bounded. The report.failed counter remains accurate regardless. If you need to debug an import with thousands of failures, fix the most common error in your source data, re-export, and run a fresh import.

Debugging a failed line

  1. Look at message to identify the error class (most often validation_failed).
  2. Use product_id to find the offending entry in your source export.
  3. Try the same payload through POST /products to get the full validation feedback in the response, the synchronous endpoint returns the precise field path in error.details, which the import log abbreviates.
  4. Fix the source data and re-run the import.

Limits and quotas

LimitValueNotes
Upload URL lifetime1 hourAfter expires_at, the SAS token stops accepting PUT requests. Re-create the import to get a fresh URL.
Lines per fileNo hard cap todayFiles over 1,000,000 lines may take a long time to ingest; consider splitting into multiple imports if you're past that range.
Concurrent imports per companyNo hard cap todayQueue your own imports if you want strict ordering.
Resource typesproduct onlyMore resource types may be added later.
File formatNDJSON onlyCSV and JSON-array formats are not supported and not currently planned.

Webhooks

Skip polling with webhooks

Subscribe to the import.completed and import.failed events on a webhook endpoint and Humind will POST you the final status payload as soon as the import reaches a terminal state. The body mirrors the Import object, so you can drop your polling loop entirely. See Webhooks › Available events.


Test mode

Imports inherit the test/live mode of the API key that created them. If your key is hmd_test_*, every product ingested by the import is isolated from your live catalog. A live import (hmd_live_*) cannot create or modify test products and vice versa. See Authentication for the full test-mode model.


Common errors

The Imports API can return any of the standard HTTP statuses, but these are the codes specific to this resource:

StatusCodeWhenFix
403insufficient_scopeKey lacks imports:write.Create a new key with imports:write.
404not_foundThe {sync_id} doesn't match an import owned by your company.Confirm the sync_id. Cross-tenant lookups also return 404.
422import_not_pendingstart or cancel was called on an import that's not in the right state (e.g. start on a processing/done/failed/cancelled import).Check status first. Each lifecycle transition is one-way.
422import_blob_missingstart was called before the NDJSON file was uploaded to upload_url.PUT the blob first, then call start.
400validation_failedThe body of POST /imports was missing resource_type or used an unsupported value.Fix the body and resend.

Next

Released under the proprietary Humind license.