Public Alpha. Easel is in early development, so expect rough edges.

Easel

← API Documentation

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

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
    inventory to 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.