Queek docs

Translate store content

Build a translation app on the Merchant API. The resource types and their ids, how to read source text, write translations safely, count progress per language, and the errors you will meet.

A translation app adds languages to a store and fills in the store's own text in each one: products, collections, pages, blog posts, menus and the words a theme prints. It uses the Merchant API, with the scopes below, from the app's server.

This page is about content a merchant owns. For the words your own theme prints, see Translating your theme. The capability and its limits are listed under Translated commerce content.

Scopes

ScopeAllows
merchant-locales-readGET /locales
merchant-locales-writeAdd, publish, unpublish and remove a language; set the default language
merchant-translations-readGET /translatable-resources and GET /translatable-resources/{type}/{id}
merchant-translations-writePUT /translations/{type}/{id} and DELETE /translations/{type}/{id}

The two translations scopes cover every resource type below. No product, page, menu, category or theme scope is needed to read or translate them. An app that only reads needs only the two read scopes. See Scopes and the translations reference.

Resource types

GET /translatable-resources?type=… lists one type at a time. Each item carries the source text the merchant wrote, split into keys.

typeWhat it isKeys
productA producttitle, description (HTML), excerpt
product_variationA product option, such as Sizetitle
product_variation_itemA value of an option, such as Largename
collectionA collectionname, description
categoryThe category name shown on a productname
pageA store pagetitle, excerpt, seo.title, seo.description
postA blog posttitle, excerpt, seo.title, seo.description
menuA navigation menuitems.<n>.label, with nested items.<n>.children.<n>.label
theme_stringThe store's theme text<theme-slug>.<dotted-key>, for example roast.cart.title

A key appears only when the source has a value for it. An empty field is not translatable, and it shows up on the item once the merchant fills it in. Each key carries a type, text or html: keep the markup of an html value and translate only its text.

page and post never overlap: page is every page that is not a blog post, and post is the blog posts. A draft page or post is listed like a published one.

theme_string is one record per store. It lists every key of the store's active theme in a single page, and its id is the store's own p_id. The theme slug is part of every key, so switching theme leaves the old theme's translations dormant until you remove them with a DELETE narrowed to those keys.

Ids

The id of an item in a list is always an integer: the record's p_id. It is the same number the record carries as p_id elsewhere, for example on a page from GET /pages. Use it as the stable identifier in your own database.

The retrieve and write routes accept that integer or the record's UUID in {id}. An id that belongs to another store answers 404 not_found, as does an unknown type. For theme_string, {id} is the store's p_id, its slug or its UUID.

Collections and categories

A store's collections and its categories are one set of rows: type=collection and type=category list the same records, with the same ids. The general product taxonomy that every store shares is never translatable per store, and never listed.

The two types keep separate translations. A translation written under category/2016 is not returned when you read collection/2016, and the reverse. Each is read by a different part of the storefront: collection supplies the name and description on collection responses, and category supplies the category name shown on a product.

Use collection as the type you offer merchants. Write category as well when you want the product's category name translated too, since the category type has only the name key.

The flow

The examples use the base URL https://api.usequeek.com/api/v1/merchant and the installation token your SDK client sends in X-Client-Key.

1. Read the store's languages

curl https://api.usequeek.com/api/v1/merchant/locales \
  -H "X-Client-Key: $TOKEN" -H "Accept: application/json"

The response lists each language with locale, name, native_name, html_lang, dir, published, is_primary and is_default, the default first. The store's source language appears with is_primary: true. It is never a translation target: asking for it is 422 translation_locale_is_primary. The language object carries no address; it does not say where shoppers reach the language.

Add a language with POST /locales and { "locale": "fr" }. It is unpublished until you publish it with PATCH /locales/fr and { "published": true }. A store publishes at most 20 languages.

2. Read the source text

curl "https://api.usequeek.com/api/v1/merchant/translatable-resources?type=product&limit=100" \
  -H "X-Client-Key: $TOKEN" -H "Accept: application/json"
{
  "data": [
    {
      "type": "product",
      "id": 4021,
      "content": [
        { "key": "title", "value": "Linen shirt", "digest": "…", "type": "text" },
        { "key": "description", "value": "<p>Soft, light linen.</p>", "digest": "…", "type": "html" }
      ],
      "machine_translation_allowed": true
    }
  ],
  "has_more": true,
  "next_cursor": "…"
}

The digest identifies the exact source value. Keep it with the text you translate.

3. Read what is already translated

curl "https://api.usequeek.com/api/v1/merchant/translatable-resources/product/4021" \
  -H "X-Client-Key: $TOKEN" -H "Accept: application/json"

The response repeats content and adds translations, one row per stored locale and key:

{
  "status": "success",
  "message": "Translatable resource retrieved",
  "data": {
    "type": "product",
    "id": 4021,
    "content": [ … ],
    "machine_translation_allowed": true,
    "translations": [
      { "key": "title", "locale": "fr", "value": "Chemise en lin", "outdated": false, "updated_at": "2026-10-18T09:12:00+00:00" }
    ]
  }
}

Without locale the response carries the translations of every language. Add ?locale=fr to narrow it to one.

A translation is outdated: true when the source value changed after it was written. The storefront keeps showing an outdated translation until you replace it, except on menu, where it is not shown, because a re-ordered menu would put one item's text on another.

4. Write translations

curl -X PUT https://api.usequeek.com/api/v1/merchant/translations/product/4021 \
  -H "X-Client-Key: $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "translations": [
      { "locale": "fr", "key": "title", "value": "Chemise en lin", "digest": "…" }
    ]
  }'

Each item names a locale, a key, the translated value and the digest you read for that key. The batch is all or nothing: if any item is wrong, nothing is written, and the error names the item. A write replaces an existing translation of the same locale and key.

To remove translations, call DELETE /translations/{type}/{id} with locales[] and keys[] to narrow it. With neither, it clears every translation of that record.

5. Publish the language

A language shoppers can pick must be published: PATCH /locales/{locale} with { "published": true }. To make it the language the store root serves, call POST /locales/{locale}/default; the language must be published first. How a language is reached is described under Which languages a store has.

Pagination

GET /translatable-resources is cursor paged. Send limit (default 15, at most 100). Each response has has_more and next_cursor; pass next_cursor back as starting_after for the next page, and stop when has_more is false. Items are ordered by p_id. A cursor belongs to one type and one store: reusing it on another type answers 422.

Counting progress per language

The list carries source text only. It cannot filter by language, and it does not say what is translated. To show "312 of 480 texts translated in French", read the items you want to count:

  1. Page through the list for a type at 100 items per page.
  2. For each item, call GET /translatable-resources/{type}/{id} once, without locale. It returns the translations of every language together.
  3. For each language, count the item's keys that have a row with outdated: false. A key with no row is missing; a row with outdated: true needs another look.
  4. Cache the counts for a short time, and drop them when your app writes or removes a translation.

The cost is one request per item, not per item per language. Every request counts against the key's rate limit, which is per minute and depends on the store's plan, so a large catalogue takes minutes on a small plan. Read a bounded number of items per type, show progress for what you have read, and read the rest in the background.

Limits

LimitValue
Items per write100
Translated value50,000 characters
key64 characters
Items per list page100
Published languages per store20

Errors

The translation_* codes answer HTTP 422. See Merchant API errors.

CodeCause
translation_digest_staleThe source changed after you read it. Re-read the item and translate again.
translation_unknown_keyThe record has no such key. It may be empty, or the key is misspelt.
translation_unknown_localeThe language has not been added to the store.
translation_locale_is_primaryThe store's source language is not a translation target.
translation_locale_is_defaultThe default language cannot be unpublished or removed. Pick another default first.
translation_locale_not_publishedOnly the primary or a published language can be the default.
translation_max_published_localesThe store already publishes 20 languages.

A write error carries the position of the failing item in error.errors, for example translations.3.digest. An unknown type, or an id from another store, is 404 not_found; a missing or unrecognised type on the list is 422 validation_failed.

Webhooks

The locales/create, locales/update, locales/destroy, translations/update and translations/delete topics exist. The public API document does not define the data they carry, so do not build on its fields. Treat each event as a signal that something changed, and read the current state from the API. See Webhooks.

Machine translation

Queek does not run a translation model for apps; a translation app brings its own. Each item carries machine_translation_allowed. It is false for a store's privacy, terms, returns and shipping policy pages, as advice to machine translators. Manual writes to those pages stay allowed.

Send only text you read from Queek to a model, so your route cannot be used to translate arbitrary text, and show merchants a suggestion before you save it.

Try it on a dev store

A dev store created with test data has products, collections, customers, coupons and shipping zones. It has no pages, posts or menus. To create a page, POST /pages with merchant-pages-create; the page is a draft, and it lists as a page. To create a post, POST /posts with merchant-posts-create. The Merchant API has no endpoint to create a menu.

On this page