Bulk Create SKU.io Products From Store Products
POST/api/storefront/instances/:id/products/create-sku-products
Create SKU.io catalog products from unmapped store products, then map them. A store product whose SKU already exists in the SKU.io catalogue is linked to it rather than duplicated.
This endpoint currently requires session authentication; Personal Access Token scope support is in progress.
This is the deliberate counterpart of auto-matching: it ADDS rows to your catalogue, so it never runs implicitly during an import.
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). Resolving to an empty selection returns 200 with No products to create and changes nothing.
Field mappings are not accepted here; the connection's stored mappings (or the defaults) are used. To override them for a single product, use POST /{product}/create-sku-product.
The response is the refreshed catalogue counts, so you can see how many products are still unmapped.
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.