Product Imports
IntentFlow supports three ways to get products in: a scheduled product feed, the API (this page), and manual upload in the dashboard. All three flow through one shared update pipeline. For a non-technical overview see Importing Products.
Stores can upsert product catalogs with an API key. CSV uploads are available in the merchant dashboard; the public API accepts JSON product objects. Changed products are queued for AI search phrase enrichment, enrichment saves generated suggestions, and then products are indexed for autocomplete.
The API offers two granularities: batch upsert (POST /product-imports, below) for many products at once, and single-product calls (PUT / DELETE /products/{external_id}) for real-time, one-at-a-time changes. Both run the identical pipeline.
Upload Products
POST /api/v1/product-imports
Content-Type: application/jsonSend your store API key with X-Api-Key or Authorization: Bearer.
You may send a single product object:
{
"external_id": "SKU-1001",
"title": "Running Shoe",
"category": "Running Shoes",
"price": 99.99,
"url": "https://shop.example/products/running-shoe",
"image_url": "https://cdn.example/sku-1001.jpg",
"in_stock": true,
"attributes": {
"color": "blue",
"material": "mesh"
}
}Or a batch:
{
"products": [
{
"external_id": "SKU-1001",
"title": "Running Shoe",
"category": "Running Shoes",
"price": 99.99,
"url": "https://shop.example/products/running-shoe",
"attributes": {
"color": "blue"
}
}
]
}Example:
curl -X POST https://app.example.com/api/v1/product-imports \
-H "X-Api-Key: if_PLACEHOLDER_KEY" \
-H "Content-Type: application/json" \
-d @products.jsonSuccessful API upserts return 200 OK:
{
"message": "Products upserted.",
"import": {
"id": "9b8f3e4a-5a3b-43f5-92db-0c1df5a24e4d",
"status": "completed",
"filename": "api-json",
"total_rows": 1,
"processed_rows": 1,
"valid_rows": 1,
"failed_rows": 0,
"skipped_rows": 0,
"failure_count": 0,
"error": null,
"created_at": "2026-07-04T10:00:00.000000Z",
"updated_at": "2026-07-04T10:00:00.000000Z",
"links": {
"self": "https://app.example.com/api/v1/product-imports/9b8f3e4a-5a3b-43f5-92db-0c1df5a24e4d"
}
}
}If an import would create more products than the store's plan allows, the import does not fail. It completes successfully, imports new products up to the plan limit, and skips the rest. The number of skipped rows is reported in skipped_rows. Free stores can hold up to 100 products. Updating existing external_id values is always allowed and never counts against the limit.
{
"message": "Products upserted.",
"import": {
"status": "completed",
"valid_rows": 150,
"failed_rows": 0,
"skipped_rows": 50,
"error": null
}
}AI phrase enrichment is also plan-limited per store. Free stores can enrich up to 5 products. Product imports still succeed after that enrichment limit is reached, but additional new or changed products are saved without queuing AI enrichment.
Product Fields
Required fields:
| Field | Description |
|---|---|
external_id | Your stable product or SKU ID. Unique per store. Reusing it updates the existing product. |
title | Product title, up to 255 characters. |
price | Numeric price, zero or greater. |
url | Absolute product URL. |
Optional fields:
| Field | Description |
|---|---|
category | Product category, up to 255 characters. Used for category autocomplete and AI phrase generation when present. |
image_url | Absolute product image URL. |
in_stock | Boolean or one of 1, 0, true, false, yes, or no. Blank or omitted defaults to in stock. |
attributes | Object or label/value string of extra product facts, such as { "color": "blue" } or size: 42; color=blue. Values are stored as strings, sent to AI enrichment, and indexed as searchable text. |
Extra top-level keys on each product object are also treated as additional attributes, unless they are standard product fields.
Internally, each product is normalized into base fields (title, description, price, identifiers, stock, URLs, and media), merchant-defined attributes, and generated search phrases. Attributes are preserved as label/value pairs so the enrichment job can use as much product context as the merchant sends.
CSV Format
CSV imports are available through the merchant dashboard. The first row must be a header row. Header names are case-insensitive.
Required columns:
| Column | Description |
|---|---|
external_id | Your stable product or SKU ID. Unique per store. Reusing it updates the existing product. |
title | Product title, up to 255 characters. |
price | Numeric price, zero or greater. |
url | Absolute product URL. |
Optional columns:
| Column | Description |
|---|---|
category | Category label used for category autocomplete results and AI phrase generation. |
image_url | Absolute product image URL. |
in_stock | 1, 0, true, false, yes, or no. Blank defaults to in stock. |
attributes | Optional label/value pairs, either JSON ({"size":"42"}) or text (size: 42; color=blue). |
| Any extra column | Stored as an additional attribute. For example, color with value blue is sent to enrichment as color: blue. |
Example:
external_id,title,category,price,url,image_url,in_stock,color
SKU-1001,Running Shoe,Running Shoes,99.99,https://shop.example/products/running-shoe,https://cdn.example/sku-1001.jpg,yes,blue
SKU-1002,Trail Boot,Boots,129.50,https://shop.example/products/trail-boot,,true,brownUpdate Or Remove A Single Product
For real-time updates, change one product at a time instead of sending a batch. Use these when a single product changes in your shop (price update, stock flip, new product). They upsert by external_id, scoped to your store, and go through the same validation, enrichment, and indexing as a batch import.
Upsert one product
PUT /api/v1/products/{external_id}
Content-Type: application/jsoncurl -X PUT https://app.example.com/api/v1/products/SKU-1001 \
-H "X-Api-Key: if_PLACEHOLDER_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Running Shoe",
"category": "Running Shoes",
"price": 89.99,
"url": "https://shop.example/products/running-shoe",
"in_stock": true,
"attributes": {
"size": "42",
"color": "blue"
}
}'The body uses the same product fields as a batch (the external_id comes from the URL). Returns 200 OK with a one-row run summary.
{
"message": "Product upserted.",
"import": {
"status": "completed",
"valid_rows": 1,
"failed_rows": 0,
"skipped_rows": 0
},
"product": {
"external_id": "SKU-1001",
"title": "Running Shoe",
"description": null,
"category": "Running Shoes",
"category_path": ["Running Shoes"],
"brand": null,
"price": "89.99",
"currency": "EUR",
"url": "https://shop.example/products/running-shoe",
"image_url": null,
"image_urls": [],
"in_stock": true,
"stock_status": "in_stock",
"attributes": {
"color": "blue",
"size": "42"
},
"suggestion_phrases": ["comfortable running shoes"],
"search_phrases": ["blue running shoes"]
}
}Remove one product
DELETE /api/v1/products/{external_id}curl -X DELETE https://app.example.com/api/v1/products/SKU-1001 \
-H "X-Api-Key: if_PLACEHOLDER_KEY"Marks the product out of stock (it stops appearing as available in autocomplete). Product data is retained. Returns 200 OK.
{
"message": "Product marked out of stock.",
"product": {
"external_id": "SKU-1001",
"in_stock": false,
"stock_status": "out_of_stock",
"attributes": {
"color": "blue",
"size": "42"
},
"suggestion_phrases": ["comfortable running shoes"],
"search_phrases": ["blue running shoes"]
}
}Check Import Status
GET /api/v1/product-imports/{import_id}Example:
curl https://app.example.com/api/v1/product-imports/9b8f3e4a-5a3b-43f5-92db-0c1df5a24e4d \
-H "X-Api-Key: if_PLACEHOLDER_KEY"Statuses:
| Status | Meaning |
|---|---|
pending | Upload accepted; waiting for a worker. |
processing | CSV rows are being validated and stored. |
completed | Import finished. Some rows may still have failed validation; check failed_rows. |
failed | The whole import failed, usually because the file was unreadable or missing required columns. |
Processing And Enrichment
CSV imports stream the file instead of loading it into memory. Valid rows are written in batches, failed rows are logged in batches, and duplicate external_id values in the same import resolve to the last row seen for that ID.
Only new or changed products are sent to AI enrichment. Category changes and additional attribute changes count as product changes. Enrichment runs on a separate queue with rate limiting, saves generated phrases, and then indexes the product so autocomplete can keep serving existing indexed data while fresh phrases are generated.
Bulk Upload Tips
For 10k to 20k products:
- Prefer one CSV per store and keep
external_idstable between uploads. - Avoid sending unchanged catalogs repeatedly; unchanged rows are skipped for enrichment.
- Keep image URLs and product URLs absolute and valid before upload.
- Poll the status URL every 10 to 30 seconds instead of retrying the upload.
- If an import reports failed rows, fix the CSV and upload again with the same
external_idvalues.