Product Images API

Upload, list, describe, and delete images for a product. Image uploads use multipart/form-data.

Endpoints


List Product Images

Retrieve all images for a product. This endpoint is not paginated.

Endpoint: GET /api/v1/products/{product_id}/images/

Scope Required: products:read

Request

curl -X GET "https://studio.easel.engineering/api/v1/products/pro_d0b6fmv28q6vn14peun0/images/" \
  -H "Authorization: Bearer your_api_key_here"

Response

Status: 200 OK

{
  "data": [
    {
      "id": "img_d0b6fmv28q6vn14peun0",
      "url": "/images/img_d0b6fmv28q6vn14peun0-large.webp",
      "alt_text": "Front of the t-shirt",
      "width": 1200,
      "height": 1200,
      "format": "webp",
      "created_at": "2023-01-01T10:00:00Z",
      "external_source": null,
      "external_id": null,
      "taken_down_at": null
    }
  ]
}

The url is relative to the storefront origin.


Upload Product Images

Upload one or more images for a product. Send the images as files in a multipart/form-data body. Every file part is stored, so you can include several images in a single request.

Endpoint: POST /api/v1/products/{product_id}/images/

Scope Required: products:write

Limits

  • The total request body must be 50 MiB or smaller.
  • Images must be no larger than 8192 pixels in either dimension. Larger images are rejected.
  • Supported formats depend on the image decoder. Common web formats such as JPEG, PNG, GIF, and WebP are accepted.

Uploaded images are downscaled and re-encoded as WebP, which keeps transparency, and report a format of webp. The derived files are named <image_id>-<size>.webp, for example img_d0b6fmv28q6vn14peun0-large.webp. Each image also has a JPEG copy, <image_id>-share.jpeg, up to 1200 pixels on its longest side, for email clients and link previews that can’t show WebP. Images uploaded before WebP report jpeg or png until Easel converts them. Photos are turned upright using their EXIF orientation, so a phone photo taken sideways is stored the right way up.

Form Fields

Field Required Description
(any name) Yes An image file; every file part is stored
external_source No System the image was imported from; requires external_id and exactly one file (see External References)
external_id No Image’s ID in that system; requires external_source

Request

curl -X POST "https://studio.easel.engineering/api/v1/products/pro_d0b6fmv28q6vn14peun0/images/" \
  -H "Authorization: Bearer your_api_key_here" \
  -H "Idempotency-Key: unique-request-id-12345" \
  -F "file=@front.jpg" \
  -F "file=@back.jpg"

Response

Status: 201 Created

{
  "data": [
    {
      "id": "img_d0b6fmv28q6vn14peun0",
      "url": "/images/img_d0b6fmv28q6vn14peun0-large.webp",
      "alt_text": "",
      "width": 1200,
      "height": 1200,
      "format": "webp",
      "created_at": "2023-01-01T10:00:00Z",
      "external_source": null,
      "external_id": null,
      "taken_down_at": null
    }
  ]
}

Error Responses

Invalid multipart form

Status: 400 Bad Request

{
  "error": "Invalid input",
  "code": "INVALID_FIELD"
}

Request too large

Status: 413 Request Entity Too Large

{
  "error": "Request body is too large",
  "code": "REQUEST_TOO_LARGE"
}

Image refused

A file Easel removed as illegal can’t be uploaded to any store.

Status: 422 Unprocessable Entity

{
  "error": "The image can't be uploaded",
  "code": "INVALID_FIELD"
}

Update Product Image

Set an image’s alt text, which describes it for screen readers and search engines. The storefront’s product page uses it, and falls back to the product’s name for an image without any. Translate it into the site’s other languages with the Translations API.

Endpoint: PUT /api/v1/products/{product_id}/images/{image_id}/

Scope Required: products:write

Request Body

Field Type Required Description
alt_text string Yes Up to 512 characters, trimmed; an empty string removes it

Request

curl -X PUT "https://studio.easel.engineering/api/v1/products/pro_d0b6fmv28q6vn14peun0/images/img_d0b6fmv28q6vn14peun0/" \
  -H "Authorization: Bearer your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"alt_text": "Front of the t-shirt"}'

Response

Status: 200 OK

The updated image.

{
  "id": "img_d0b6fmv28q6vn14peun0",
  "url": "/images/img_d0b6fmv28q6vn14peun0-large.webp",
  "alt_text": "Front of the t-shirt",
  "width": 1200,
  "height": 1200,
  "format": "webp",
  "created_at": "2023-01-01T10:00:00Z",
  "external_source": null,
  "external_id": null,
  "taken_down_at": null
}

Error Responses

Alt text too long

Status: 400 Bad Request

{
  "error": "Invalid input",
  "code": "INVALID_FIELD"
}

Image not found, or not on this product

Status: 404 Not Found

{
  "error": "Resource not found",
  "code": "NOT_FOUND"
}

Delete Product Image

Delete an image from a product.

Endpoint: DELETE /api/v1/products/{product_id}/images/{image_id}/

Scope Required: products:write

Request

curl -X DELETE "https://studio.easel.engineering/api/v1/products/pro_d0b6fmv28q6vn14peun0/images/img_d0b6fmv28q6vn14peun0/" \
  -H "Authorization: Bearer your_api_key_here"

Response

Status: 204 No Content

Error Responses

Image not found

Status: 404 Not Found

{
  "error": "Resource not found",
  "code": "NOT_FOUND"
}

Image Object

Fields

Field Type Description
id string Image public ID
url string Relative URL to the large image
alt_text string Alt text; empty when the image has none
width integer Width in pixels of the image at url
height integer Height in pixels of the image at url
format string Image format: webp, or jpeg or png for an image uploaded before WebP that is not yet converted
created_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
taken_down_at string|null ISO 8601 timestamp of when Easel took the image down, or null while it hasn’t. Read-only. See Takedowns

Takedowns

Easel hosts your store’s content, so under the EU Digital Services Act its staff may take an image down that’s illegal or against Easel’s terms (see Moderation). A taken-down image stays on its product, with taken_down_at set, but isn’t served at any size, so its url stops loading, on the storefront too. Delete it, or upload another in its place. The statement of reasons was emailed to the store’s owners, and the dashboard keeps it under Settings › Activity › Easel decisions; the activity log’s taken_down event, on the store with part image, names the category in reason.