Bulk Map Store Products By SKU
POST/api/storefront/instances/:id/products/bulk-map
Auto-match unmapped store products to EXISTING SKU.io products that already carry the same SKU. No human choice is involved and no catalog product is ever created — products with no SKU.io counterpart are simply left unmapped and counted as skipped.
This endpoint currently requires session authentication; Personal Access Token scope support is in progress.
Selecting what to act on — the shared selection contract used by this endpoint, POST /bulk-map, POST /create-sku-products, PUT /archive, PUT /unarchive, POST /archiveable, PUT /delete and PUT /deleteable:
{"ids": [16, 17]}— an explicit list of store-product ids.{"apply_to_all": true}— every product on the instance that the product list would show with no filters, i.e. every NON-ARCHIVED product.{"apply_to_all": true, "filters": { ... }}— every product matching those filters, however many pages that spans. This is "select everything matching what I am looking at".
filters carries the SAME keys the product list accepts, but written as bare column names with no filter[...] wrapper — e.g. {"mapping_status": "unmapped", "status": "active", "search": "door", "search_columns": "sku,unified_title", "price.greater_than": 100, "filter_groups": "<base64 tree>"}. A nested {"filter": {...}} object, literal filter[col] keys, and a JSON string of the whole object are all accepted too. Display and paging keys (page, per_page, sort, included, total, table_specifications, query) are ignored — the selection is never limited to one page. search is deliberately NOT ignored.
Two rules worth knowing:
- If
filtersdoes not mentionarchived, archived products are excluded, exactly as the list hides them by default. A sweep can therefore never reach a product the list was not showing. - A
filterspayload that cannot be parsed, or afilter_groupstree that cannot be parsed, is rejected with 422 rather than being treated as "no filters" — which would have acted on every row.
Body fields: ids (array of integers, optional), apply_to_all (boolean, optional), filters (object or JSON string, optional — only consulted when apply_to_all is true). Sending none of them matches nothing and is a no-op.
The response counts what happened: processed is how many unmapped products were considered, mapped how many were linked, skipped the remainder.
This is NOT the same as PUT /map, which applies one explicitly chosen target per row. Neither replaces the other.
Permission: integrations.update.
Authentication: Bearer token (session or PAT).
Request
Responses
- 200
- 401
- 403
- 404
- 422
- 429
OK
Response Headers
Unauthenticated — the bearer token is missing, revoked, expired, or malformed. Never retry automatically; fix the credential. See the Errors guide.
Forbidden — the token lacks a required scope, the endpoint is not available to API tokens, or the user behind the token lacks the permission. A human must adjust the token scopes or user permissions; do not retry.
Not found — no record with the given identifier (or the route does not exist). Verify the ID before retrying.
Validation failed — the body is a field → messages map (Laravel shape) or the platform envelope with a stable machine-readable code. Fix the payload and resubmit.
Rate limited — platform limit is 1,000 requests/min; individual tokens may carry lower limits. Honor the Retry-After header before retrying. See the Rate Limits guide.