Products
Products API
Manage the product catalog for the authenticated store. Products include metadata, status flags, and variant information with pricing.
List Products
Retrieve a paginated list of products.
GET /api/apps/v1/products
Authentication
Required. Must include valid app credentials.
Scope
read:products
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
limit | integer | 50 | Items per page (max 250) |
status | string | -- | Filter by product status: active or inactive |
since | string (ISO 8601) | -- | Only return products updated on or after this date |
category_id | integer | -- | Filter by category ID |
price_min | number | -- | Only return products with at least one variant priced at or above this value |
price_max | number | -- | Only return products with at least one variant priced at or below this value |
search | string | -- | Search products by name (partial match) |
sort | string | created_at:desc | Sort results. Format: column or column:asc/column:desc. Allowed columns: created_at, updated_at, name |
Price filtering
Price filters check against variant prices (from product_variant_option_combinations), not a product-level price. A product matches if any of its non-deleted variants fall within the specified range. You can use price_min and price_max independently or together.
Example Request
curl -X GET "https://api.selorax.io/api/apps/v1/products?status=active&category_id=5&search=rose" \
-H "Authorization: Bearer <token>"Or using client credentials with price range and sorting:
curl -X GET "https://api.selorax.io/api/apps/v1/products?price_min=100&price_max=500&sort=name:asc" \
-H "X-Client-Id: sx_app_..." \
-H "X-Client-Secret: sx_secret_..." \
-H "X-Store-Id: 22"Response
{
"data": [
{
"product_id": 150,
"name": "Premium Rose Bouquet",
"slug": "premium-rose-bouquet",
"category_id": 5,
"brand_id": null,
"is_active": 1,
"is_visible": 1,
"created_at": "2025-03-10T08:00:00.000Z",
"updated_at": "2025-06-12T14:30:00.000Z"
},
{
"product_id": 149,
"name": "Seasonal Flower Basket",
"slug": "seasonal-flower-basket",
"category_id": 5,
"brand_id": 2,
"is_active": 1,
"is_visible": 1,
"created_at": "2025-03-08T12:00:00.000Z",
"updated_at": "2025-05-20T09:15:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 50,
"total": 2
},
"status": 200
}Response Fields
| Field | Type | Description |
|---|---|---|
product_id | integer | Unique product identifier |
store_id | integer | Store the product belongs to |
name | string | Product title |
slug | string | URL-friendly product slug |
category_id | integer or null | Associated category ID |
brand_id | integer or null | Associated brand ID |
is_active | integer (0 or 1) | Whether the product is active |
is_visible | integer (0 or 1) | Whether the product is visible on the storefront |
created_at | string (ISO 8601) | When the product was created |
updated_at | string (ISO 8601) | When the product was last updated |
Node.js Example
const response = await fetch(
"https://api.selorax.io/api/apps/v1/products?status=active&category_id=5&search=rose",
{ headers: { Authorization: "Bearer sx_at_..." } }
);
const { data, pagination } = await response.json();
console.log(`Found ${pagination.total} products`);Python Example
import requests
response = requests.get(
"https://api.selorax.io/api/apps/v1/products",
headers={"Authorization": "Bearer sx_at_..."},
params={"status": "active", "category_id": 5, "search": "rose"},
)
data = response.json()
for product in data["data"]:
print(f"{product['product_id']}: {product['name']}")Get Product
Retrieve a single product by ID, including its variants.
GET /api/apps/v1/products/:product_id
Authentication
Required. Must include valid app credentials.
Scope
read:products
Path Parameters
| Parameter | Type | Description |
|---|---|---|
product_id | integer | The product ID to retrieve |
Example Request
curl -X GET "https://api.selorax.io/api/apps/v1/products/150" \
-H "Authorization: Bearer <token>"Response
{
"data": {
"product_id": 150,
"sku_id": 450,
"slug": "premium-rose-bouquet",
"brand_id": null,
"category_id": 5,
"store_id": 22,
"name": "Premium Rose Bouquet",
"description": "<p>A hand-crafted bouquet of premium roses.</p>",
"short_description": "Hand-crafted premium roses",
"is_bundle": 0,
"is_active": 1,
"is_visible": 1,
"is_featured": 0,
"is_bestseller": 1,
"is_best_deals": 0,
"created_at": "2025-03-10T08:00:00.000Z",
"updated_at": "2025-06-12T14:30:00.000Z",
"variants": [
{
"sku_id": 450,
"product_id": 150,
"variant_name": "Small / Red",
"sku_code": "PRB-SM-RED",
"price": 500.00,
"compare_at_price": 600.00,
"available": 1,
"quantity": 25,
"is_default": 1,
"images": null,
"weight": 0.5,
"width": null,
"depth": null,
"height": null,
"position": 1,
"free_shipping": 0
},
{
"sku_id": 451,
"product_id": 150,
"variant_name": "Large / Red",
"sku_code": "PRB-LG-RED",
"price": 750.00,
"compare_at_price": 900.00,
"available": 1,
"quantity": 12,
"is_default": 0,
"images": null,
"weight": 0.8,
"width": null,
"depth": null,
"height": null,
"position": 2,
"free_shipping": 0
}
]
},
"status": 200
}Product Detail Fields
The single product response includes all list fields plus:
| Field | Type | Description |
|---|---|---|
sku_id | integer | Default variant SKU ID |
store_id | integer | Store the product belongs to |
description | string or null | Full product description (may contain HTML) |
short_description | string or null | Brief product summary |
is_bundle | integer (0 or 1) | Whether this is a bundle product |
is_featured | integer (0 or 1) | Whether the product is featured |
is_bestseller | integer (0 or 1) | Whether the product is marked as a bestseller |
is_best_deals | integer (0 or 1) | Whether the product appears in best deals |
Variant Fields
| Field | Type | Description |
|---|---|---|
sku_id | integer | Variant SKU identifier |
product_id | integer | Parent product ID |
variant_name | string | Combined option labels (e.g. "Large / Red") |
sku_code | string | Human-readable SKU code |
price | number | Current selling price |
compare_at_price | number or null | Original price for comparison (strikethrough pricing) |
available | integer (0 or 1) | Whether the variant is available for purchase |
quantity | integer | Stock quantity on hand |
is_default | integer (0 or 1) | Whether this is the default variant |
images | string or null | Variant-specific images (JSON) |
weight | number or null | Weight in kg |
width | number or null | Width dimension |
depth | number or null | Depth dimension |
height | number or null | Height dimension |
position | integer | Display order position |
free_shipping | integer (0 or 1) | Whether free shipping applies to this variant |
Error Responses
| Code | Status | Meaning |
|---|---|---|
invalid_token | 401 | Token is expired or invalid |
insufficient_scope | 403 | App does not have read:products scope |
If the product_id does not exist or does not belong to the authenticated store:
{
"message": "Product not found.",
"status": 404
}Create Product
Create a new product with an initial variant.
POST /api/apps/v1/products
Scope
write:products
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Product name |
description | string | No | Full product description (HTML allowed) |
short_description | string | No | Brief summary |
category_id | integer | No | Category ID |
brand_id | integer | No | Brand ID |
is_active | boolean | No | Whether active (default: true) |
is_visible | boolean | No | Whether visible on storefront (default: true) |
is_featured | boolean | No | Whether featured (default: false) |
variant | object | Yes | Initial variant (see below) |
Variant fields:
| Field | Type | Required | Description |
|---|---|---|---|
price | number | Yes | Selling price |
variant_name | string | No | Variant label (default: "Default") |
sku_code | string | No | SKU code |
compare_at_price | number | No | Original/compare price |
quantity | integer | No | Stock quantity (default: 0) |
images | string | No | Comma-separated image keys |
weight | number | No | Weight in kg |
cost | number | No | Cost price |
free_shipping | boolean | No | Free shipping flag |
Example Request
curl -X POST "https://api.selorax.io/api/apps/v1/products" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Premium Rose Bouquet",
"description": "<p>Hand-crafted premium roses</p>",
"category_id": 5,
"variant": {
"price": 500,
"compare_at_price": 600,
"sku_code": "PRB-SM-RED",
"quantity": 25,
"weight": 0.5
}
}'Response (201)
{
"data": {
"product_id": 155,
"sku_id": 460,
"name": "Premium Rose Bouquet",
"slug": "premium-rose-bouquet",
"store_id": 22
},
"status": 201
}Slug generation
The product slug is auto-generated from the name and guaranteed unique within the store.
Update Product
Update product metadata. Only provided fields are updated.
PUT /api/apps/v1/products/:product_id
Scope
write:products
Request Body
| Field | Type | Description |
|---|---|---|
name | string | Product name (slug auto-regenerated) |
description | string | Full description |
short_description | string | Brief summary |
category_id | integer | Category ID |
brand_id | integer | Brand ID |
is_active | boolean | Active status |
is_visible | boolean | Storefront visibility |
is_featured | boolean | Featured flag |
is_bestseller | boolean | Bestseller flag |
is_best_deals | boolean | Best deals flag |
Example Request
curl -X PUT "https://api.selorax.io/api/apps/v1/products/150" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Premium Rose Bouquet - Updated",
"is_featured": true,
"category_id": 8
}'Update Variant
Update a specific product variant's price, stock, images, or dimensions.
PUT /api/apps/v1/products/:product_id/variants/:sku_id
Scope
write:products
Request Body
| Field | Type | Description |
|---|---|---|
variant_name | string | Variant label |
sku_code | string | SKU code |
price | number | Selling price |
compare_at_price | number | Compare-at price |
quantity | integer | Stock quantity (available auto-set based on this) |
images | string | Comma-separated image keys |
weight | number | Weight in kg |
width | number | Width |
height | number | Height |
depth | number | Depth |
position | integer | Display order |
free_shipping | boolean | Free shipping flag |
cost | number | Cost price |
Example Request
curl -X PUT "https://api.selorax.io/api/apps/v1/products/150/variants/451" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"price": 800,
"quantity": 50,
"images": "products/prb-lg-red-1.jpg,products/prb-lg-red-2.jpg"
}'Add Variant
Add a new variant to an existing product.
POST /api/apps/v1/products/:product_id/variants
Scope
write:products
Request Body
Same fields as the variant object in Create Product, with price required.
Example Request
curl -X POST "https://api.selorax.io/api/apps/v1/products/150/variants" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"variant_name": "Extra Large / White",
"sku_code": "PRB-XL-WHT",
"price": 950,
"quantity": 10
}'Response (201)
{
"data": {
"sku_id": 465,
"product_id": 150,
"variant_name": "Extra Large / White",
"price": 950,
"quantity": 10
},
"status": 201
}Delete Variant
Soft-delete a product variant. Cannot delete the last remaining variant.
DELETE /api/apps/v1/products/:product_id/variants/:sku_id
Scope
write:products
Error Responses
| Status | Meaning |
|---|---|
| 400 | Cannot delete the last variant |
| 404 | Product or variant not found |
Delete Product
Soft-delete a product (deactivates and hides from storefront).
DELETE /api/apps/v1/products/:product_id
Scope
write:products
Upload Product Images
Upload images for a product. Images are auto-converted to WebP and thumbnails are generated. Use the returned stored_key values in variant images fields.
POST /api/apps/v1/products/:product_id/images
Scope
write:products
Content-Type
multipart/form-data
Limits
- Max 5 files per request
- Max 10MB per file
- Accepted types: JPEG, PNG, GIF, WebP, SVG, BMP, TIFF
Path Parameters
| Parameter | Type | Description |
|---|---|---|
product_id | integer | The product ID to upload images for |
Form Fields
| Field | Type | Required | Description |
|---|---|---|---|
files | file(s) | Yes | Image files (use multiple files fields for multiple images) |
Example Request
curl -X POST "https://api.selorax.io/api/apps/v1/products/150/images" \
-H "Authorization: Bearer <token>" \
-F "[email protected]" \
-F "[email protected]"Response
{
"message": "2 image(s) uploaded",
"data": [
{
"id": 234,
"file_url": "https://pub-xxxxx.r2.dev/media/22/products/a1b2c3d4.webp",
"thumbnail_url": "https://pub-xxxxx.r2.dev/media/22/products/thumbs/a1b2c3d4.webp",
"stored_key": "media/22/products/a1b2c3d4.webp",
"width": 1200,
"height": 1200,
"file_size": 45678
},
{
"id": 235,
"file_url": "https://pub-xxxxx.r2.dev/media/22/products/e5f6g7h8.webp",
"thumbnail_url": "https://pub-xxxxx.r2.dev/media/22/products/thumbs/e5f6g7h8.webp",
"stored_key": "media/22/products/e5f6g7h8.webp",
"width": 800,
"height": 800,
"file_size": 32100
}
],
"status": 200
}Image Processing
- JPEG, PNG, BMP, TIFF are auto-converted to WebP (quality 85) for optimal size
- GIF, WebP, SVG are stored as-is
- A 300px thumbnail is generated for each image
- Original dimensions (
width,height) are preserved in metadata
Using uploaded images
After uploading, use the stored_key values in the variant's images field as a comma-separated string:
curl -X PUT ".../products/150/variants/451" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"images": "media/22/products/a1b2c3d4.webp,media/22/products/e5f6g7h8.webp"}'Error Responses
| Status | Meaning |
|---|---|
| 400 | No files provided or invalid file type |
| 404 | Product not found |
| 413 | Storage quota exceeded |