Theme API
https://theme.hystore.com.br/v1 reads and changes a store's code theme. It is
the API the hystore tool uses, open to your integrations and to CI. The full
list of routes, with their formats, is in the OpenAPI.
1. Create a key
- In the dashboard, open Customization > Theme access > Create key.
- Give it a name (for example, Site deploy).
- Pick what it may do:
| Scope | Allows |
|---|---|
theme:read |
read files, versions and media; check; preview links |
theme:write |
change the draft, images and fonts; bring a version back |
theme:publish-request |
ask to publish (approved in the dashboard) |
- Pick how long it lasts (30 to 365 days) and, if you want, the IPs that may use it.
- Copy the key (
hys_theme_...). It shows only once: keep it like a password, in a vault or in the CI secrets.
2. Test the key
export HYSTORE_THEME_TOKEN=hys_theme_...
curl https://theme.hystore.com.br/v1/me -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
{
"storeId": "0f558e5e-...",
"store": { "name": "My Store", "domain": "mystore.hystore.com.br" },
"scopes": ["theme:read", "theme:write"],
"credential": { "id": "...", "kind": "key" }
}
The store is always the key's store: no route takes a store id. A key never does more than the role of the person who created it allows, and revoking it in the dashboard cuts it at once.
Read the theme
# Draft files, with sha256 and size, and the draft revision
curl https://theme.hystore.com.br/v1/theme/files -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
# One file
curl "https://theme.hystore.com.br/v1/theme/file?path=sections/hero.liquid" -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
# Every text file at once (with content and hash)
curl https://theme.hystore.com.br/v1/theme/export -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
A file comes like this:
{ "path": "sections/hero.liquid", "content": "<section class=\"hero\">...", "sha256": "3f1c..." }
Write safely
Send back the sha256 you read as baseSha:
curl -X PUT "https://theme.hystore.com.br/v1/theme/file?path=sections/hero.liquid" \
-H "Authorization: Bearer $HYSTORE_THEME_TOKEN" \
-H "Content-Type: application/json" \
-d '{"content":"<section class=\"hero\"><h1>{{ section.settings.title }}</h1></section>\n{% schema %}{\"name\":\"Hero\",\"settings\":[{\"id\":\"title\",\"type\":\"text\",\"label\":\"Title\"}]}{% endschema %}","baseSha":"3f1c..."}'
- If the file changed since you read it, the answer is
409with the current hash, and nothing is written. Read again, merge and write. baseSha: nullmeans the file must not exist yet (create without erasing someone else's).- Without
baseSha, the write overwrites.
Several files at once, all or nothing (up to 100 changes):
curl -X POST https://theme.hystore.com.br/v1/theme/files \
-H "Authorization: Bearer $HYSTORE_THEME_TOKEN" \
-H "Content-Type: application/json" \
-d '{"changes":[{"path":"locales/pt.json","content":"{\"home\":{\"title\":\"Welcome\"}}","baseSha":"9a0b..."},{"path":"sections/old.liquid","content":null}],"expectedRevision":12}'
content: null deletes. expectedRevision (optional) refuses the write if
another one got there first since you read.
A file that breaks a rule gets 422 with each problem:
{ "errors": [{ "code": "html_event_handler", "message": "Event attributes (onclick, onload...) are not allowed. Use data-hystore-* components.", "path": "sections/hero.liquid", "line": 3 }] }
Check, preview and publish
# Checks the whole draft
curl -X POST https://theme.hystore.com.br/v1/theme/check -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
# A 24-hour link that opens the draft on the store's address
curl -X POST https://theme.hystore.com.br/v1/theme/preview -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
# Asks to publish (someone approves in the dashboard)
curl -X POST https://theme.hystore.com.br/v1/theme/publish-requests \
-H "Authorization: Bearer $HYSTORE_THEME_TOKEN" \
-H "Content-Type: application/json" \
-d '{"note":"Black Friday banner"}'
Versions
# The last 30 published versions
curl https://theme.hystore.com.br/v1/theme/versions -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
# A file as it was in version 3
curl "https://theme.hystore.com.br/v1/theme/versions/3/file?path=styles/theme.css" -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
# Brings version 3 back to the draft (the live store does not change)
curl -X POST https://theme.hystore.com.br/v1/theme/versions/3/restore -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
Images and fonts
# Lists them, with each public address
curl https://theme.hystore.com.br/v1/theme/assets -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
# Uploads (the body is the file; the type is read from the content)
curl -X PUT "https://theme.hystore.com.br/v1/theme/asset?path=assets/banner.webp" \
-H "Authorization: Bearer $HYSTORE_THEME_TOKEN" \
--data-binary @banner.webp
# Deletes
curl -X DELETE "https://theme.hystore.com.br/v1/theme/asset?path=assets/banner.webp" -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
PNG, JPEG, WebP, GIF and AVIF images up to 3 MB; WOFF and WOFF2 fonts up to 1 MB. SVG is not accepted as a media file.
Node.js example
Changes a text of the theme and asks to publish. Works on Node.js 18 or newer, with no libraries.
const API = 'https://theme.hystore.com.br/v1'
const headers = { Authorization: `Bearer ${process.env.HYSTORE_THEME_TOKEN}`, 'Content-Type': 'application/json' }
async function call(method, path, body) {
const res = await fetch(API + path, { method, headers, body: body && JSON.stringify(body) })
const data = await res.json()
if (!res.ok) throw new Error(`${res.status} ${JSON.stringify(data.errors ?? data)}`)
return data
}
// 1. Reads the English texts file
const file = await call('GET', '/theme/file?path=locales/en.json')
const texts = JSON.parse(file.content)
// 2. Changes the title and writes, with the hash it read (409 if someone changed it first)
texts.home = { ...texts.home, title: 'Season 5 is here' }
await call('PUT', '/theme/file?path=locales/en.json', { content: JSON.stringify(texts, null, 2), baseSha: file.sha256 })
// 3. Asks to publish
await call('POST', '/theme/publish-requests', { note: 'Season 5 title' })
console.log('Done: approve it in Customization > Code theme.')
HYSTORE_THEME_TOKEN=hys_theme_... node update-title.mjs
Routes
| Route | Scope |
|---|---|
GET /v1/me |
any |
GET /v1/theme, GET /v1/theme/files, GET /v1/theme/file?path= |
read |
GET /v1/theme/export |
read |
PUT and DELETE /v1/theme/file?path=, POST /v1/theme/files |
write |
POST /v1/theme/check |
read |
POST /v1/theme/preview |
read |
GET /v1/theme/versions, GET /v1/theme/versions/{n}/file?path= |
read |
POST /v1/theme/versions/{n}/restore |
write |
GET /v1/theme/assets, PUT and DELETE /v1/theme/asset?path= |
read, write |
POST /v1/theme/publish-requests |
publish-request |
Error answers
Every error has the same shape and never carries internal details:
{ "errors": [{ "code": "SCOPE_REQUIRED", "message": "This credential cannot do this (needs theme:write)." }] }
| Status | Means |
|---|---|
400 |
malformed request (path, body) |
401 |
no key, a wrong, expired or revoked key |
403 |
the key lacks the scope, the IP is not on the list, the plan lacks the module, or the request came from a web page |
404 |
the file or the version does not exist |
409 |
the file or the draft changed since you read it |
413 |
file or batch too big |
422 |
the file breaks a theme rule (with the line) |
429 |
too many calls; wait the seconds in Retry-After |
Limits
- 120 reads and 30 writes per minute per key; above that,
429withRetry-After. - Files: 256 KB each, 400 files, 5 MB of text. Media: images 3 MB, fonts 1 MB, 100 files, 40 MB.
