Product Variants API
Manage product variants for your products. A variant is a specific version of
a product, for example a size or color, with its own price and inventory.
A variant is named by its option values, in the order
the options were created, joined by ", ", such as "Medium, Green". Renaming a value renames every variant that uses
it, so to rename a variant, rename its values. A product without options has a
single variant, named "Default", which the storefront doesn't show.
Options make variants as the dashboard does. A product
created with options has every combination of
their values as a variant, and adding a
value to an option makes
its new combinations. Variants can also be created one at a time with
Create Product Variant.
To stop selling a variant, set its inventory to 0, or
delete it unless it has been ordered: a variant on
any order stays for the order history, so setting its inventory to 0 is the
way to retire it. Deleting a
value gives its variants
another value of the option, merging those left with the same values, which
isn't possible once any of them has been ordered.
Endpoints
- List Product Variants -
GET /api/v1/products/{product_id}/variants/ - Create Product Variant -
POST /api/v1/products/{product_id}/variants/ - Get Product Variant -
GET /api/v1/products/{product_id}/variants/{variant_id}/ - Update Product Variant -
PUT /api/v1/products/{product_id}/variants/{variant_id}/ - Delete Product Variant -
DELETE /api/v1/products/{product_id}/variants/{variant_id}/
Digital files have their own endpoints. See
Digital Files.
List Product Variants
Retrieve all variants for a specific product. A product can have at most 100
variants, more only if it had them before the limit, so variant lists are not
paginated and has_more is always false.
Endpoint: GET /api/v1/products/{product_id}/variants/
Scope Required: products:read
Path Parameters
| Parameter | Type | Description |
|---|---|---|
product_id |
string | Product public ID (e.g., pro_d0b6fmv28q6vn14peun0) |
Request
curl -X GET "https://studio.easel.engineering/api/v1/products/pro_d0b6fmv28q6vn14peun0/variants/" \
-H "Authorization: Bearer your_api_key_here"
Response
Status: 200 OK
{
"data": [
{
"id": "prv_d0b6fmv28q6vn14peun0",
"name": "Small, Red",
"price": 1999,
"sale": null,
"total_inventory": 50,
"delivery": "physical",
"created_at": "2023-01-01T10:00:00Z",
"updated_at": "2023-01-01T10:15:00Z",
"external_source": null,
"external_id": null,
"weight_grams": null,
"sku": "TS-RED-S",
"barcode": "4006381333931"
},
{
"id": "prv_xk3mq7r41p8nt26zv9w0",
"name": "Large, Blue",
"price": 2499,
"sale": {
"price": 1999
},
"total_inventory": 25,
"delivery": "physical",
"created_at": "2023-01-01T10:30:00Z",
"updated_at": "2023-01-01T11:00:00Z",
"external_source": null,
"external_id": null,
"weight_grams": null,
"sku": null,
"barcode": null
}
],
"has_more": false
}
Error Responses
Product not found
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Failed to get product
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Create Product Variant
Create a variant for a product, from one value of each of its options. A
product without options takes no values, and can have only one variant.
A product can have at most 100 variants. A product that already has more,
from before the limit, keeps them but can't add another.
Endpoint: POST /api/v1/products/{product_id}/variants/
Scope Required: products:write
Path Parameters
| Parameter | Type | Description |
|---|---|---|
product_id |
string | Product public ID (e.g., pro_d0b6fmv28q6vn14peun0) |
Request Body
{
"price": 2199,
"inventory": 10,
"sku": "TS-GRN-M",
"option_value_ids": [
"pov_d0b6fmv28q6vn14peun0",
"pov_d0b6fmv28q6vn14peun1"
]
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
price |
integer | Yes | Price in the store's smallest currency unit (must be non-negative) |
inventory |
integer | No | Stock count (defaults to 0, must be non-negative) |
option_value_ids |
string[] | When the product has options | Exactly one value ID of each of the product's options |
sku |
string|null | No | The merchant's own code for the variant, unique within the store (see SKU and Barcode) |
barcode |
string|null | No | The variant's GTIN, EAN or UPC, 8 to 14 digits (see SKU and Barcode) |
external_source |
string | No | System the record was imported from; requires external_id (see External References) |
external_id |
string | No | Record's ID in that system; requires external_source |
Request
curl -X POST "https://studio.easel.engineering/api/v1/products/pro_d0b6fmv28q6vn14peun0/variants/" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-request-id-12345" \
-d '{
"price": 2199,
"inventory": 10,
"sku": "TS-GRN-M",
"option_value_ids": [
"pov_d0b6fmv28q6vn14peun0",
"pov_d0b6fmv28q6vn14peun1"
]
}'
Optional Headers:
Idempotency-Key: Unique string to prevent duplicate variant creation (see Idempotency)
Response
Returns the created variant.
Status: 201 Created
{
"id": "prv_j2k5n8p1q4r7t0w3x6z9",
"name": "Medium, Green",
"price": 2199,
"sale": null,
"total_inventory": 10,
"delivery": "physical",
"created_at": "2023-01-01T12:00:00Z",
"updated_at": "2023-01-01T12:00:00Z",
"external_source": null,
"external_id": null,
"weight_grams": null,
"sku": "TS-GRN-M",
"barcode": null
}
Error Responses
Invalid JSON body
Status: 400 Bad Request
{
"error": "Invalid JSON body",
"code": "INVALID_JSON"
}
Option values missing or not on the product, or a second variant of a
product without options
Status: 400 Bad Request
{
"error": "Invalid input",
"code": "INVALID_FIELD"
}
Invalid price or inventory
Status: 400 Bad Request
{
"error": "Price must be non-negative",
"code": "INVALID_FIELD"
}
Invalid SKU or barcode
Status: 400 Bad Request
{
"error": "Barcode must be 8 to 14 digits",
"code": "INVALID_FIELD"
}
SKU already used by another variant of the store
Status: 409 Conflict
{
"error": "Another variant already has this SKU",
"code": "DUPLICATE"
}
Product already has 100 variants
Status: 409 Conflict
{
"error": "A product can have at most 100 variants",
"code": "TOO_MANY_VARIANTS"
}
Failed to create variant
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Get Product Variant
Retrieve a single product variant by its ID.
Endpoint: GET /api/v1/products/{product_id}/variants/{variant_id}/
Scope Required: products:read
Path Parameters
| Parameter | Type | Description |
|---|---|---|
product_id |
string | Product public ID (e.g., pro_d0b6fmv28q6vn14peun0) |
variant_id |
string | Variant public ID (e.g., prv_d0b6fmv28q6vn14peun0) |
Request
curl -X GET "https://studio.easel.engineering/api/v1/products/pro_d0b6fmv28q6vn14peun0/variants/prv_d0b6fmv28q6vn14peun0/" \
-H "Authorization: Bearer your_api_key_here"
Response
Status: 200 OK
{
"id": "prv_d0b6fmv28q6vn14peun0",
"name": "Small, Red",
"price": 1999,
"sale": null,
"total_inventory": 50,
"delivery": "physical",
"created_at": "2023-01-01T10:00:00Z",
"updated_at": "2023-01-01T10:15:00Z",
"external_source": null,
"external_id": null,
"weight_grams": null,
"sku": "TS-RED-S",
"barcode": "4006381333931"
}
Error Responses
Variant not found
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Failed to get variant
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Update Product Variant
Update an existing product variant's price, inventory, sale, weight, SKU
or barcode. Every field is replaced, so send the ones to keep too.
Endpoint: PUT /api/v1/products/{product_id}/variants/{variant_id}/
Scope Required: products:write
Path Parameters
| Parameter | Type | Description |
|---|---|---|
product_id |
string | Product public ID (e.g., pro_d0b6fmv28q6vn14peun0) |
variant_id |
string | Variant public ID (e.g., prv_d0b6fmv28q6vn14peun0) |
Request Body
{
"price": 2299,
"inventory": 45,
"sale": {
"price": 1999
},
"weight_grams": 2200,
"sku": "TS-RED-S",
"barcode": "4006381333931"
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
price |
integer | Yes | Price in the store's smallest currency unit (must be non-negative) |
inventory |
integer | Yes | Stock count (must be non-negative). It replaces the count, so to add units received, add them to the current total_inventory |
sale |
object|null | No | Sale price. Omit or set to null to end the sale |
weight_grams |
integer|null | No | The variant's own weight in grams, from 0 to 1,000,000, such as for a larger size. Omit or null to weigh what its product does (see Weight) |
sku |
string|null | No | The merchant's own code for the variant, unique within the store. Omit, null or "" to clear it (see SKU and Barcode) |
barcode |
string|null | No | The variant's GTIN, EAN or UPC, 8 to 14 digits. Omit, null or "" to clear it |
Omit sale or set it to null to remove an existing sale. Supplying a
sale.price sets or updates the sale.
Starting a sale below the regular price queues a
wishlist sale alert for every opted-in
customer with the variant on their account wishlist. Changing the price of a
running sale alerts no one again.
Request
curl -X PUT "https://studio.easel.engineering/api/v1/products/pro_d0b6fmv28q6vn14peun0/variants/prv_d0b6fmv28q6vn14peun0/" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-request-id-67890" \
-d '{
"price": 2299,
"inventory": 45,
"sale": {
"price": 1999
},
"weight_grams": 2200,
"sku": "TS-RED-S",
"barcode": "4006381333931"
}'
Optional Headers:
Idempotency-Key: Unique string to prevent duplicate variant updates (see Idempotency)
Response
Returns the updated variant.
Status: 200 OK
{
"id": "prv_d0b6fmv28q6vn14peun0",
"name": "Small, Red",
"price": 2299,
"sale": {
"price": 1999
},
"total_inventory": 45,
"delivery": "physical",
"created_at": "2023-01-01T10:00:00Z",
"updated_at": "2023-01-01T13:30:00Z",
"external_source": null,
"external_id": null,
"weight_grams": 2200,
"sku": "TS-RED-S",
"barcode": "4006381333931"
}
Restock Emails
If the variant has stock after the update (total_inventory greater than
zero), Easel emails every shopper waiting on that variant and removes them
from the waitlist. The emails are sent in the background, so they never
change the response; a waitlist read failure is logged and ignored. Shoppers
who were already emailed are not emailed again.
See Restock Waitlist for the endpoints that report
waiting counts.
Error Responses
Invalid JSON body
Status: 400 Bad Request
{
"error": "Invalid JSON body",
"code": "INVALID_JSON"
}
Invalid price, inventory, or sale price
Status: 400 Bad Request
{
"error": "Price must be non-negative",
"code": "INVALID_FIELD"
}
Invalid SKU or barcode
Status: 400 Bad Request
{
"error": "Barcode must be 8 to 14 digits",
"code": "INVALID_FIELD"
}
SKU already used by another variant of the store
Status: 409 Conflict
{
"error": "Another variant already has this SKU",
"code": "DUPLICATE"
}
Variant not found
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Failed to update variant
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Delete Product Variant
Delete a variant, as the dashboard does. A variant with stock can be deleted:
its total_inventory goes with it, with no need to set it to 0 first.
Shoppers' carts lose it, and it leaves restock waitlists and wishlists.
Three variants can't be deleted, and each is refused with 409 Conflict:
- A product's last variant, since a product needs one to be sold.
- A variant on any order, since the order history needs it. Set its
inventoryto 0 with Update Product Variant to
stop selling it instead. - A variant holding a digital file: delete the file
first.
There is no bulk delete; delete each variant with its own request.
Endpoint: DELETE /api/v1/products/{product_id}/variants/{variant_id}/
Scope Required: products:write
Request
curl -X DELETE "https://studio.easel.engineering/api/v1/products/pro_d0b6fmv28q6vn14peun0/variants/prv_d0b6fmv28q6vn14peun0/" \
-H "Authorization: Bearer your_api_key_here"
Response
Status: 204 No Content
Error Responses
The product's last variant
Status: 409 Conflict
{
"error": "A product keeps at least one variant",
"code": "CONFLICT"
}
The variant has been ordered
Status: 409 Conflict
{
"error": "Variants someone has ordered can't be deleted or merged",
"code": "CONFLICT"
}
The variant holds a digital file
Status: 409 Conflict
{
"error": "Delete the variant's digital file first",
"code": "CONFLICT"
}
Product or variant not found
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Product Variant Object
Fields
| Field | Type | Description |
|---|---|---|
id |
string | Variant public ID |
name |
string | The variant's option values joined by ", ", or "Default" for a product without options |
price |
integer | Regular price in the store's smallest currency unit |
sale |
object|null | Sale information if the variant is on sale |
total_inventory |
integer | Current stock count. A made-to-order product stays on sale at 0 |
delivery |
string | physical or digital |
created_at |
string | ISO 8601 timestamp |
updated_at |
string | ISO 8601 timestamp |
external_source |
string|null | System the record was imported from (see External References) |
external_id |
string|null | Record's ID in that system |
weight_grams |
integer|null | The variant's own weight in grams, or null when it weighs what its product does |
sku |
string|null | The merchant's own code for the variant, or null for none |
barcode |
string|null | The variant's GTIN, EAN or UPC, or null for none |
Sale Object
When a variant is on sale, the sale field contains:
| Field | Type | Description |
|---|---|---|
price |
integer | Sale price in the store's smallest currency unit |
Delivery
Delivery comes from the product. Physical variants are shipped and
carry inventory. Digital variants are delivered as a download and do not need
stock.
SKU and Barcode
sku is the merchant's own code for a variant, the one a warehouse, a
shipping app or a spreadsheet knows it by. It is any printable text up to 64
characters, trimmed of surrounding spaces, and no two variants of a store can
share one, compared without regard to case. A variant
copied from another starts without one.
barcode is the number printed under the variant's barcode: a GTIN, as an
EAN-8, UPC-A, EAN-13 or GTIN-14, 8 to 14 digits. Spaces and hyphens are taken
out. The check digit isn't checked, so a store's own in-house codes fit too,
and two variants can share a barcode.
Both are null in a variant object when it has none. The dashboard's data
export includes them as the sku and barcode columns of
product_variants.