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

Easel

← API Documentation

Translations API

A site has one main language, set by its language, and can be offered in
other languages too. Each other language serves the same storefront under a
path prefix, such as /fr/ for French, with your products, pages, and
checkout in that language. Shoppers who browse in it get their order email
in it too.

The storefront's own text, such as buttons and checkout labels, is already
translated. Your content is not: these endpoints list what there is to
translate into each language and save your translations. Until a field is
translated, the storefront shows it in the main language.

Related resources:

Endpoints

What Can Be Translated

A translation is one field of one piece of content in one language. Its id
names the content: a public ID for anything that has one, a page type for an
info page, or home for the home page.

Content id Fields Longest translation
Home page home hero_content, seo_title, seo_description 65,536, 255, and 1,023 characters
Info page Page type, such as about or shipping content 65,536 characters
Collection col_... name, description 256 and 2,047 characters
Product pro_... name, description 256 and 2,047 characters
Product image img_... alt_text 512 characters
Product option pop_... name 256 characters
Option value pov_... value 256 characters
Storefront filter sfl_... name 256 characters
Shipping rate shr_... name 100 characters
Gift option gft_... name, description 256 and 1,000 characters

The info page types are about, shipping, returns, terms, privacy,
contact, order_confirmation, and announcement_bar. Only fields with
text in the main language can be translated: a product with no description
has no description to translate, and an image without
alt text has none.

Status

Every translation has a status:

Status Meaning
missing Not translated. The storefront shows the main-language text
outdated Translated, but you have since changed the main-language text. The storefront still shows the old translation
done Translated from the current main-language text

Each translation keeps the main-language text it was translated from, as
translated_from. Editing that text in the main language makes the
translation outdated until you save a new one; source is the text to
translate, and comparing it with translated_from shows what changed.

Turning a Language Off

Removing a language from a site stops serving the storefront in it and
hides its translations from these endpoints, but deletes none of them.
Adding the language back restores them, each with the status it would have
had: any you translated from text you have since changed come back
outdated.

Shoppers who used a language the site no longer offers see the main
language, and their email is sent in it.


Set Site Languages

Replace the languages a site is offered in besides its main one.

Endpoint: PUT /api/v1/sites/{site_id}/languages/

Permission Required: Write

Request Body

Field Type Required Description
languages array Yes Language codes, such as ["de", "fr"]. An empty array offers only the main language

Each language must be one the storefront's own text is translated into:
bg, cs, da, de, el, en, es, et, fi, fr, ga, hr, hu,
it, lt, lv, mt, nl, pl, pt, ro, sk, sl, or sv. A site
can offer at most 32 languages besides its main one, which can't be in the
list. Repeated languages are ignored, and the site lists them sorted.

Request

curl -X PUT "https://studio.easel.engineering/api/v1/sites/sit_d0b6fmv28q6vn14peun0/languages/" \
  -H "Authorization: Bearer your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"languages": ["fr", "de"]}'

Response

Status: 204 No Content

Get Site then lists "languages": ["de", "fr"].

Error Responses

Missing languages

Status: 400 Bad Request

{
  "error": "languages is required",
  "code": "MISSING_FIELD"
}

Unsupported or main language

Status: 400 Bad Request

{
  "error": "languages can't include the site's main language",
  "code": "INVALID_FIELD"
}

A code that isn't a language, one the storefront isn't translated into, or
more than 32 languages are refused the same way, each with its own message.

Site not found

Status: 404 Not Found


List Translations

List a page of a site's translations. By default it lists the work left to
do, missing and outdated, in every one of the site's other languages, so a
translator can work down the list.

Translations are listed in the order the dashboard shows them: the home
page, info pages, collections, products with their options and option
values, shipping rates, then gift options.

Endpoint: GET /api/v1/sites/{site_id}/translations/

Permission Required: Read

Query Parameters

Parameter Type Description
status string todo (the default: missing and outdated), missing, outdated, done, or all
language string Only this language, which must be one of the site's other languages
product string Only this product's content: its name and description, its images' alt text, its options, and their values
after string The cursor of the last translation on the previous page
before string The cursor of the first translation on the next page, to page backward
limit integer Number of translations to return (default 10, max 100)

Translations are paged by their cursor, not their id, since one piece of
content has a translation for each field and language. after and before
can't be used together. has_more says whether there are more past the page
in the direction it was read: after it, or before it with before.

A translation saved while you page through a todo list drops out of it, so
keep passing the cursor of the last translation you read, not of the last
one you saved.

Request

curl -X GET "https://studio.easel.engineering/api/v1/sites/sit_d0b6fmv28q6vn14peun0/translations/?language=fr&limit=2" \
  -H "Authorization: Bearer your_api_key_here"

Response

Status: 200 OK

{
  "data": [
    {
      "id": "pro_d0b6fmv28q6vn14peun0",
      "field": "name",
      "language": "fr",
      "label": "Wool Blanket",
      "source": "Wool Blanket",
      "value": null,
      "translated_from": null,
      "translated_at": null,
      "status": "missing",
      "cursor": "pro_d0b6fmv28q6vn14peun0:name:fr"
    },
    {
      "id": "pov_d0b6fmv28q6vn14peup0",
      "field": "value",
      "language": "fr",
      "label": "Wool Blanket › Color",
      "source": "Forest Green",
      "value": "Vert",
      "translated_from": "Green",
      "translated_at": "2026-10-18T12:00:00Z",
      "status": "outdated",
      "cursor": "pov_d0b6fmv28q6vn14peup0:value:fr"
    }
  ],
  "has_more": true
}

Error Responses

Invalid query parameter

Status: 400 Bad Request

{
  "error": "Invalid 'language' parameter: must be one of the site's other languages",
  "code": "INVALID_REQUEST_ERROR"
}

An unknown status, a cursor for a translation the site no longer has,
both after and before, or a limit over 100 are refused the same way.

Site or product not found

Status: 404 Not Found


Summarize Translations

Count what is done and left to do in each of a site's other languages, in
the order the site lists them. It reads all of the site's content, so use it
to show progress, not before every request.

Endpoint: GET /api/v1/sites/{site_id}/translations/summary/

Permission Required: Read

Request

curl -X GET "https://studio.easel.engineering/api/v1/sites/sit_d0b6fmv28q6vn14peun0/translations/summary/" \
  -H "Authorization: Bearer your_api_key_here"

Response

Status: 200 OK

{
  "data": [
    {
      "language": "de",
      "total": 148,
      "done": 0,
      "missing": 148,
      "outdated": 0
    },
    {
      "language": "fr",
      "total": 148,
      "done": 121,
      "missing": 20,
      "outdated": 7
    }
  ]
}

done, missing, and outdated add up to total. A site with no other
languages returns an empty data. The list isn't paged, since a site offers
at most 32 other languages.


Save Translations

Save up to 100 translations at once, all or none. Each is saved as
translated from the field's current main-language text.

Endpoint: PUT /api/v1/sites/{site_id}/translations/

Permission Required: Write

Request Body

Field Type Required Description
data array Yes 1 to 100 translations to save

Each translation in data:

Field Type Required Description
id string Yes The content, as in What Can Be Translated
field string Yes The field to translate
language string Yes One of the site's other languages
value string Yes The translation. Surrounding whitespace is trimmed, and "" deletes the translation
source string No The main-language text you translated. The translation is refused if it is no longer the current text

value must be present even to delete, so a translation left out by mistake
never deletes one. Pass source, the source you listed, whenever the
translation may be saved after a delay, such as by a translator working
through a list: if you change the text meanwhile, the save is refused with
SOURCE_CHANGED instead of saving a translation of old text as done.

Request

curl -X PUT "https://studio.easel.engineering/api/v1/sites/sit_d0b6fmv28q6vn14peun0/translations/" \
  -H "Authorization: Bearer your_api_key_here" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: unique-request-id-97531" \
  -d '{
    "data": [
      {
        "id": "pro_d0b6fmv28q6vn14peun0",
        "field": "name",
        "language": "fr",
        "value": "Couverture en laine",
        "source": "Wool Blanket"
      },
      {
        "id": "pov_d0b6fmv28q6vn14peup0",
        "field": "value",
        "language": "fr",
        "value": ""
      }
    ]
  }'

Response

Status: 200 OK

{
  "saved": 1,
  "deleted": 1
}

Error Responses

Translations refused

Status: 400 Bad Request

When any translation is refused, none are saved. errors lists every
refused one by its index in data, so you can fix them all and send the
batch again:

{
  "error": "2 of 3 translations were refused, so none were saved",
  "code": "TRANSLATIONS_REFUSED",
  "errors": [
    {
      "index": 0,
      "code": "SOURCE_CHANGED",
      "message": "The main-language text changed after this was translated; translate the current source"
    },
    {
      "index": 2,
      "code": "TOO_LONG",
      "message": "value must be at most 256 characters"
    }
  ]
}
Code Meaning
MISSING_FIELD id, field, or language is missing
INVALID_FIELD value is missing
INVALID_LANGUAGE language isn't a language code
UNKNOWN_ID No content on this site has this id; it may have been deleted
UNKNOWN_FIELD The content has no such field. The message lists the fields it has
TOO_LONG value is longer than the field takes
LANGUAGE_NOT_OFFERED The site isn't offered in language. Add it first
NO_SOURCE The field is empty in the main language, so there is nothing to translate
SOURCE_CHANGED source is no longer the field's main-language text

Empty or too large a batch

Status: 400 Bad Request

{
  "error": "Save at most 100 translations at a time",
  "code": "INVALID_FIELD"
}

An empty data is refused with MISSING_FIELD.

Site not found

Status: 404 Not Found


Translation Object

Field Type Description
id string The content translated: a public ID, a page type, or home
field string The field translated, such as name
language string The language translated into, such as fr
label string Context for a translator, such as the product and option an option value belongs to
source string The field's current text in the site's main language
value string|null The translation, or null when there is none
translated_from string|null The main-language text value was translated from. It differs from source when the translation is outdated
translated_at string|null ISO 8601 timestamp of when value was saved
status string missing, outdated, or done (see Status)
cursor string Names the translation for after and before