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 size | Frequency | Recommended endpoint |
|---|---|---|
| 1 product | One-off or webhook-driven | POST /products |
| Up to 500 products | Occasional bulk update | POST /products/batch |
| 500+ products, or any size on a recurring schedule | Daily or weekly full sync | POST /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.
If products belong to product families, create the groups through POST /product-groups before starting the import. Every NDJSON line accepts the same group_memberships field as a synchronous product write. Creating groups requires catalog:write; running the import requires imports:write.
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
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"
}'| Field | Type | Required | Description |
|---|---|---|---|
resource_type | 'product' | Yes | What kind of resource the file contains. Only product is supported today. |
format | 'ndjson' | Yes | The on-the-wire format. Only ndjson (newline-delimited JSON) is supported today. |
Response 201 Created
{
"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"
}| Field | Type | Description |
|---|---|---|
sync_id | string | The handle for this import. Use it in the path of every subsequent call. |
status | string | Always pending at this point, the file hasn't been uploaded yet. |
upload_url | string | The 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_at | ISO 8601 | When 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","metafields":[{"namespace":"content","key":"skin_types","type":"list.single_line_text_field","value":"[\"dry\",\"sensitive\"]"}],"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"}]}Product and variant metafields use the same format and validation rules as synchronous writes. See Product metafields. HTML values must use type: "html"; list values must be JSON-encoded strings.
Every field of the synchronous product schema is accepted per line, including translations (with per-language metafield overrides) and related_products / complementary_products.
Each line can also use "status": "unlisted" for a product that remains available through its direct storefront URL but must not be indexed or recommended by Humind. The import pipeline applies the same product status semantics as synchronous writes.
Product group_memberships also use the synchronous format. Referenced groups and options must already exist. A line with an unknown reference fails with product_group_not_found or product_group_option_not_found, while the other lines continue processing.
{"external_id":"SOFA-BLUE","title":"Blue sofa","group_memberships":[{"group_external_id":"sofa-model-1","selected_options":[{"option_external_id":"color","value":"Blue"}]}],"variants":[{"external_id":"SOFA-BLUE-DEFAULT","price":1290,"currency":"EUR"}]}Request
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/x-ndjson" \
-H "x-ms-blob-type: BlockBlob" \
--upload-file products.ndjsonThe 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_logsafter 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
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
{
"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
curl https://api.thehumind.com/public/v1/imports/b5f9e3a2-7c1d-4e8a-9b3f-0c2d4e5f6a7b \
-H "Authorization: Bearer hmd_live_..."Response 200 OK: while processing
{
"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
{
"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
| Field | Type | Description |
|---|---|---|
sync_id | string | Stable 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_products | integer | Number 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_products | integer | Number 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.created | integer | Number of products that didn't exist before and were inserted. |
report.updated | integer | Number of products that already existed (matched on external_id) and were updated in place. Lines identical to what was last imported for that product (nothing to write) are counted here too. |
report.failed | integer | Number of lines that failed validation or upsert. This count is exact even if some entries are no longer present in error_logs. |
error_logs | object[] | The most recent failure entries. Capped at the last 100. See Error log entries. |
upload_url | string | Returned only by POST /imports. Not echoed by GET. |
expires_at | ISO 8601 | Upload URL expiry. Returned only by POST /imports. |
started_at | ISO 8601 | null | When start was called. null while pending. |
completed_at | ISO 8601 | null | When the import reached done, failed, or cancelled. null until then. |
created_at | ISO 8601 | When the import was created (call to POST /imports). |
State machine
pending ──start──▶ processing ──finish──▶ done
│ │
│ ├──fatal error──▶ failed
│ └──cancel──────▶ cancelled
└──cancel──▶ cancelledOnce 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.
Enrichment after the import
When an import reaches done, the products are stored and visible in your dashboard right away. The AI enrichment (search embedding, generated questions) runs just after, in a background job, for the products that were created or whose indexed fields changed. A product listed in the minutes following an import may therefore not be recommended by the assistant yet; it is once the enrichment pass has covered it. Lines that did not change since the previous import are not rewritten and keep their enrichment.
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
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
{
"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.
| Field | Type | Description |
|---|---|---|
message | string | Human-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_id | string | The 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: …). |
timestamp | ISO 8601 | When 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
- Look at
messageto identify the error class (most oftenvalidation_failed). - Use
product_idto find the offending entry in your source export. - Try the same payload through
POST /productsto get the full validation feedback in the response, the synchronous endpoint returns the precise field path inerror.details, which the import log abbreviates. - Fix the source data and re-run the import.
Limits and quotas
| Limit | Value | Notes |
|---|---|---|
| Upload URL lifetime | 1 hour | After expires_at, the SAS token stops accepting PUT requests. Re-create the import to get a fresh URL. |
| Lines per file | No hard cap today | Files 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 company | No hard cap today | Queue your own imports if you want strict ordering. |
| Resource types | product only | More resource types may be added later. |
| File format | NDJSON only | CSV 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:
| Status | Code | When | Fix |
|---|---|---|---|
403 | insufficient_scope | Key lacks imports:write. | Create a new key with imports:write. |
404 | not_found | The {sync_id} doesn't match an import owned by your company. | Confirm the sync_id. Cross-tenant lookups also return 404. |
422 | import_not_pending | start 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. |
422 | import_blob_missing | start was called before the NDJSON file was uploaded to upload_url. | PUT the blob first, then call start. |
400 | validation_failed | The body of POST /imports was missing resource_type or used an unsupported value. | Fix the body and resend. |
Product feed URL
No code on your side? Instead of calling the four endpoints above, expose the same NDJSON file at a stable HTTPS URL and let Humind pull it.
- In the dashboard, open Settings > Catalog > Catalog import, then Product feed.
- Paste the URL of your NDJSON file (a token in the query string is fine) and pick a refresh frequency (every 1, 2, 3, 4, 6, 8, 12 or 24 hours).
- Save. The first pull starts immediately; the page shows the pull status and the last import's report (created, updated, failed, error log). Import now forces a pull between two scheduled runs.
Each pull is a regular import: same validation, same upsert by external_id, same import.completed / import.failed webhooks. Rules:
- HTTPS only, public internet host (private or local addresses are rejected). Redirects are followed (up to 3).
- The response body must be NDJSON: the first non-empty line must be a JSON object. HTML login pages, JSON arrays and empty files are rejected before anything is imported.
- Maximum file size: 200 MB. Maximum download time: 5 minutes (headers and body).
- One import at a time per feed: if the previous file is still being processed when the next pull is due, that pull is skipped.
- Pull-side failures (unreachable URL, HTTP 4xx/5xx, wrong format) are shown on the page and do not retry until the next scheduled pull. Per-line validation errors are reported like any import.
The feed and the API write to the same catalog; you can mix them (feed for the full catalog, POST /products from a webhook for urgent updates).
Next
- Products: the product schema each NDJSON line must follow.
- Authentication: generate an
imports:writekey. - Errors: full error code reference.