To start using our API, you'll need to register for an account.
Here you'll find detailed documentation for each available endpoint.
POST /api/v3/products
| Field | Type | Description |
|---|---|---|
id |
integer | External ID of the product (required) |
name |
string | Name of the product (required) |
description |
string | Detailed description of the product |
image |
string | URL of the product image |
suggested_category_name |
string | Suggested category name for the product |
brand |
string | Brand name of the product |
active |
boolean | Whether the product is active (true) or inactive (false) |
gtin |
string | Global Trade Item Number (GTIN) of the product |
gtins |
array of strings | Array of Global Trade Item Numbers (GTINs) for the product. If gtin is not provided but gtins are, the first GTIN from the array will be used as the primary gtin. |
On success, returns the created product with status code 201 (Created).
On error, returns validation errors with status code 422 (Unprocessable Entity).
curl https://admin.sortillus.com/api/v3/products \
-X POST \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"product": {
"id": 6,
"name": "your product name",
"suggested_category_name": "your suggested category name",
"brand": "product brand",
"description": "long product description",
"image": "https://some.com/productid.image.jpg",
"gtin": "1234567890123",
"gtins": ["1234567890123", "1234567890124"]
}
}'
{
"sortillus_id": 101,
"external_id": 6,
"name": "your product name",
"description": "long product description",
"image": "https://some.com/productid.image.jpg",
"canonical_id": null,
"category_external_id": null,
"sortillus_category_id": null,
"active": true,
"state": "new",
"updated_at": "2025-06-12T18:08:59Z",
"created_at": "2025-06-12T18:08:58Z",
"recommendations": [],
"recommended_at": null
}
PATCH /api/v3/products/:id
Update an existing product by its external ID. Only the provided fields will be updated.
Note: Parameters can be sent either wrapped in a product object or directly at the root level. Both formats are supported for backward compatibility.
| Parameter | Type | Description |
|---|---|---|
id |
integer | External ID of the product to update (required) |
| Field | Type | Description |
|---|---|---|
name |
string | Updated name of the product |
description |
string | Updated description of the product |
image |
string | URL of the updated product image. To clear the image, send an empty string "". |
category_id |
string | Full external ID (e.g., "L4_2345") or regular external ID of the category to assign to the product. The endpoint will first try to find by full_external_id, then fall back to external_id if not found. |
active |
boolean | Whether the product is active (true) or inactive (false) |
canonical |
boolean | Make this product the canonical for its cluster (true) or leave unchanged (false/not provided) |
external_canonical_product_id |
string | External ID of the product to use as canonical for this product's cluster |
gtin |
string | Global Trade Item Number (GTIN) of the product. To clear the GTIN, send an empty string "". If both gtin and gtins are provided, gtin takes precedence. |
gtins |
array of strings | Array of Global Trade Item Numbers (GTINs) for the product. If gtin is not provided but gtins are, the first non-empty GTIN from the array will be used as the primary gtin. To clear all GTINs, send an empty array []. |
On success, returns the updated product with status code 200 (OK).
On error, returns validation errors with status code 422 (Unprocessable Entity).
If the product is not found, returns status code 404 (Not Found).
If the category is not found, returns status code 422 (Unprocessable Entity).
If the external canonical product is not found, returns status code 422 (Unprocessable Entity).
If required parameters are missing (e.g., missing product wrapper when using strict format), returns status code 400 (Bad Request).
Note: The id parameter in the request body (if provided) is ignored. The product is identified by the id in the URL path only.
curl https://admin.sortillus.com/api/v3/products/6 \
-X PATCH \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"product": {
"name": "Updated product name",
"description": "Updated product description",
"active": true,
"category_id": "L4_2345"
}
}'
curl https://admin.sortillus.com/api/v3/products/6 \
-X PATCH \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"name": "Updated product name",
"description": "Updated product description",
"active": true,
"category_id": "L4_2345"
}'
curl https://admin.sortillus.com/api/v3/products/6 \
-X PATCH \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"product": {
"gtin": "1234567890123",
"gtins": ["1234567890123", "9876543210987"]
}
}'
curl https://admin.sortillus.com/api/v3/products/6 \
-X PATCH \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"product": {
"gtin": "",
"gtins": []
}
}'
curl https://admin.sortillus.com/api/v3/products/6 \
-X PATCH \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"product": {
"image": ""
}
}'
curl https://admin.sortillus.com/api/v3/products/6 \
-X PATCH \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"product": {
"canonical": true
}
}'
curl https://admin.sortillus.com/api/v3/products/6 \
-X PATCH \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"product": {
"external_canonical_product_id": "123"
}
}'
{
"sortillus_id": 101,
"external_id": 6,
"name": "Updated product name",
"description": "Updated product description",
"image": "https://some.com/productid.image.jpg",
"canonical_id": null,
"category_external_id": "L4_2345",
"sortillus_category_id": 123,
"active": true,
"state": "ready",
"updated_at": "2025-06-12T18:08:59Z",
"created_at": "2025-06-12T18:08:58Z",
"recommendations": [],
"recommended_at": null
}
Retrieve detailed information about a specific product by its external ID.
| Parameter | Type | Description |
|---|---|---|
id |
integer | External ID of the product to retrieve (required) |
On success, returns the product details with status code 200 (OK).
If the product is not found, returns status code 404 (Not Found).
The response includes a recommendations array when the product has recommended products. Each recommendation contains:
| Field | Type | Description |
|---|---|---|
product_id |
integer | External ID of the recommended product (not the internal sortillus_id) |
explanation |
string | Human-readable explanation for why this product is recommended |
reason_json |
object (optional) | Additional metadata about the recommendation (e.g., similarity score, source) |
curl https://admin.sortillus.com/api/v3/products/6 \
-H "Authorization: Bearer " \
-H "Content-Type: application/json"
{
"sortillus_id": 101,
"external_id": 6,
"name": "your product name",
"description": "long product description",
"image": "https://some.com/productid.image.jpg",
"canonical_id": null,
"category_external_id": null,
"sortillus_category_id": null,
"active": true,
"state": "new",
"updated_at": "2025-06-12T18:08:59Z",
"created_at": "2025-06-12T18:08:58Z",
"recommendations": [],
"recommended_at": null
}
{
"sortillus_id": 101,
"external_id": 6,
"name": "your product name",
"description": "long product description",
"image": "https://some.com/productid.image.jpg",
"canonical_id": null,
"category_external_id": "L4_2345",
"sortillus_category_id": 123,
"active": true,
"state": "recommended_products",
"updated_at": "2025-06-12T18:08:59Z",
"created_at": "2025-06-12T18:08:58Z",
"recommendations": [
{
"product_id": 789,
"explanation": "Same model; different color.",
"reason_json": {"source": "embedding", "score": 0.92}
},
{
"product_id": 456,
"explanation": "Bundle frequently bought together."
},
{
"product_id": 123,
"explanation": "Complementary product"
}
],
"recommended_at": "2025-06-12T18:10:00Z"
}
POST /api/v3/products/:id/recommend
Trigger the product recommendation process for a specific product identified by its internal product ID (sortillus_id). The domain_id is automatically determined from the access token provided in the Authorization header. This endpoint initiates the AASM state machine transition to start generating product recommendations asynchronously.
| Parameter | Type | Description |
|---|---|---|
id |
integer | Internal product ID (sortillus_id) of the product to trigger recommendations for (required) |
| Header | Value | Description |
|---|---|---|
Authorization |
Bearer <access_token> |
Your domain access token (required) |
Content-Type |
application/json |
Content type header |
On success, returns a confirmation message with status code 200 (OK).
If the product is not found, returns status code 404 (Not Found).
If the access token is missing or invalid, returns status code 401 (Unauthorized).
If the recommendation process fails to start (e.g., invalid product state, AASM guard conditions not met), returns status code 422 (Unprocessable Entity).
| Field | Type | Description |
|---|---|---|
message |
string | Confirmation message indicating the recommendation process was triggered |
product_id |
integer | The internal database ID of the product (same as the id parameter) |
external_id |
integer | The external ID of the product |
state |
string | The current AASM state of the product after triggering recommendations (typically recommending_products) |
should_recommend_products enabled for recommendations to work properly (checked by AASM guards).GET /api/v3/products/:id endpoint to see when recommendations are complete.domain_id is automatically determined from the access token, so you don't need to include it in the URL.
curl https://admin.sortillus.com/api/v3/products/6/recommend \
-X POST \
-H "Authorization: Bearer " \
-H "Content-Type: application/json"
{
"message": "Recommendation process triggered successfully",
"product_id": 12345,
"external_id": 6,
"state": "recommending_products"
}
{
"error": "Product not found"
}
{
"error": "Invalid access token"
}
{
"error": "Failed to trigger recommendations",
"message": "Detailed error message"
}
Retrieve a paginated list of products for your domain. Each product includes its current processing state.
| Parameter | Type | Description |
|---|---|---|
page |
integer | Page number (default: 1) |
per_page |
integer | Items per page (default: 1000) |
aasm_states |
string / array |
Filter products by AASM state(s). Accepts a comma-separated list
(?aasm_states=ready,error) or array form
(?aasm_states[]=ready&aasm_states[]=error). Only
states defined on the model are permitted; invalid values return
422 Unprocessable Entity.
When this filter or updated_before/updated_after is provided, the response omits the total_pages and total_count keys to improve performance.
|
updated_before |
string (ISO8601) | |
updated_after |
string (ISO8601) |
Return only products updated on or after this timestamp (e.g.,
2025-09-01T00:00:00Z). Can be combined with updated_before to form a range.
When supplied, the response omits total_pages and total_count in meta.
|
Return only products updated on or before this timestamp (e.g.,
2025-09-15T00:00:00Z). When supplied, the response omits
total_pages and total_count in meta. Results are ordered by
updated_at DESC, id DESC.
|
Returns a list of products with their id, external_id, processing state, timestamps, optional category_external_id, and external_canonical_id (the external_id of the canonical product if this product is a duplicate), along with pagination metadata.
Note: If the aasm_states or any of updated_before/updated_after is supplied, the meta object will only contain current_page; total_pages and total_count are omitted.
Results are ordered by updated_at DESC with id DESC as a tiebreaker.
| Field | Type | Description |
|---|---|---|
id |
integer | Internal product ID |
external_id |
integer | External product ID |
state |
string | Current processing state |
updated_at |
string (ISO8601) | Last update timestamp |
created_at |
string (ISO8601) | Creation timestamp |
category_external_id |
string/integer | External ID of the product's category (if any) |
external_canonical_id |
integer/null | The external_id of the canonical product if this product is a duplicate; null for canonical products. |
curl https://admin.sortillus.com/api/v3/products?per_page=100&aasm_states=ready,error \
-H "Authorization: Bearer " \
-H "Content-Type: application/json"
curl https://admin.sortillus.com/api/v3/products?updated_before=2025-09-15T00:00:00Z&per_page=1000 \
-H "Authorization: Bearer " \
-H "Content-Type: application/json"
curl https://admin.sortillus.com/api/v3/products?updated_after=2025-09-01T00:00:00Z&updated_before=2025-09-15T00:00:00Z&per_page=1000 \
-H "Authorization: Bearer " \
-H "Content-Type: application/json"
{
"products": [
{
"id": 101,
"external_id": 6,
"state": "ready",
"updated_at": "2025-06-12T18:08:59Z",
"created_at": "2025-06-12T18:08:58Z",
"category_external_id": "L4_2345",
"external_canonical_id": null
},
{
"id": 102,
"external_id": 7,
"state": "error",
"updated_at": "2025-06-12T17:00:00Z",
"created_at": "2025-06-12T16:50:00Z",
"category_external_id": null,
"external_canonical_id": 6
}
],
"meta": {
"current_page": 1
}
}
Retrieve a paginated list of shop categories for your domain. By default, returns all categories (including those without attributes). Use the with_attributes parameter to filter to only categories that have attributes assigned. Categories are ordered by the most recently created attribute, with categories without attributes appearing last.
| Parameter | Type | Description |
|---|---|---|
page |
integer | Page number (default: 1) |
level |
integer | Filter categories by level (e.g., 1 for top-level categories) |
parent_external_id |
integer | Filter categories by parent external ID |
search |
string | Search categories by name (case-insensitive) |
with_attributes |
boolean | If true, 1, or yes, returns only categories that have at least one product attribute assigned. Default: false (returns all categories) |
Returns a list of categories with their attributes and options, along with pagination metadata. Categories are ordered by the most recently created attribute (categories without attributes appear last, ordered by updated_at). Categories without attributes will have an empty attributes array and last_attribute_created_at will be null.
| Field | Type | Description |
|---|---|---|
id |
integer | Internal category ID |
external_id |
integer | External category ID |
full_external_id |
string | Full external ID of the category |
level |
integer | Category level in the hierarchy |
name |
string | Category name |
en_name |
string | English category name |
en_description |
string | English category description |
updated_at |
string (ISO8601) | Last update timestamp |
created_at |
string (ISO8601) | Creation timestamp |
last_attribute_created_at |
string (ISO8601) or null | Timestamp of the most recently created attribute. null if the category has no attributes. |
attributes |
array | Array of attributes assigned to this category |
| Field | Type | Description |
|---|---|---|
id |
integer | Internal attribute ID |
name |
string | Attribute name |
external_id |
integer | External attribute ID |
measurement |
string | Measurement unit for the attribute |
description |
string | Attribute description |
options |
array | Array of options for this attribute |
| Field | Type | Description |
|---|---|---|
id |
integer | Internal option ID |
name |
string | Option name |
external_id |
integer | External option ID |
measurement |
string | Measurement unit for the option |
# Get all categories
curl https://admin.sortillus.com/api/v3/shop/categories?page=1&level=1 \
-H "Authorization: Bearer " \
-H "Content-Type: application/json"
# Get only categories with attributes
curl https://admin.sortillus.com/api/v3/shop/categories?with_attributes=true \
-H "Authorization: Bearer " \
-H "Content-Type: application/json"
{
"categories": [
{
"id": 1,
"external_id": 2345,
"full_external_id": "L4_2345",
"level": 4,
"name": "Electronics",
"en_name": "Electronics",
"en_description": "Electronic devices and components",
"updated_at": "2024-01-15T10:30:00Z",
"created_at": "2024-01-01T00:00:00Z",
"last_attribute_created_at": "2024-01-15T10:30:00Z",
"attributes": [
{
"id": 1,
"name": "Color",
"external_id": 100,
"measurement": null,
"description": "Product color",
"options": [
{
"id": 1,
"name": "Red",
"external_id": 101,
"measurement": "free_text"
},
{
"id": 2,
"name": "Blue",
"external_id": 102,
"measurement": "free_text"
}
]
},
{
"id": 2,
"name": "Size",
"external_id": 200,
"measurement": "cm",
"description": "Product size",
"options": [
{
"id": 3,
"name": "25",
"external_id": 201,
"measurement": "cm"
}
]
}
]
}
],
"meta": {
"current_page": 1,
"total_pages": 1,
"total_count": 1
}
}
Creates a new category or updates an existing one. The endpoint matches or initializes by external_id within your domain. All required fields must be provided.
| Field | Type | Description |
|---|---|---|
external_id |
integer | External category ID. Required. Used to find or create within your domain. Must be a positive integer. |
name |
string | Category name. Required. |
en_name |
string | English category name. Required. Used for vectorization and embeddings. |
en_description |
string | English category description. Required. Used for vectorization and embeddings. |
description |
string | Category description. Optional. |
external_parent_id |
integer | External parent category ID. Optional. If provided, sets this category as a child of the parent. |
On create, returns the category with status code 201 Created. On update, returns 200 OK.
On validation error (e.g., missing required fields, invalid parent), returns 422 Unprocessable Entity.
curl https://admin.sortillus.com/api/v3/shop/categories \
-X POST \
-H "Authorization: Bearer " \
-H "Content-Type: application/json" \
-d '{
"category": {
"external_id": 456,
"name": "Electronics",
"en_name": "Electronics",
"en_description": "Electronic devices and components",
"description": "Long category description",
"external_parent_id": 20
}
}'
{
"id": 123,
"external_id": 456,
"name": "Electronics",
"description": "Long category description",
"en_name": "Electronics",
"en_description": "Electronic devices and components",
"parent_id": 20,
"full_external_id": "L2_456",
"level": 2,
"created_at": "2025-06-12T18:08:58Z",
"updated_at": "2025-06-12T18:08:59Z"
}