# 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](/openapi.json).

## 1. Create a key

1. In the dashboard, open **Customization > Theme access > Create key**.
2. Give it a name (for example, *Site deploy*).
3. 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) |

4. Pick how long it lasts (30 to 365 days) and, if you want, the IPs that may
   use it.
5. Copy the key (`hys_theme_...`). It shows **only once**: keep it like a
   password, in a vault or in the CI secrets.

> [!WARNING]
> The key never goes in a browser or in a repository. The API refuses requests
> from web pages (with an `Origin` header): call it from servers, CI and tools.

## 2. Test the key

```bash
export HYSTORE_THEME_TOKEN=hys_theme_...
curl https://theme.hystore.com.br/v1/me -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
```

```json
{
  "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

```bash
# 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:

```json
{ "path": "sections/hero.liquid", "content": "<section class=\"hero\">...", "sha256": "3f1c..." }
```

## Write safely

Send back the `sha256` you read as `baseSha`:

```bash
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 `409` with the current
  hash, and nothing is written. Read again, merge and write.
- `baseSha: null` means 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):

```bash
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:

```json
{ "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

```bash
# 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

```bash
# 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

```bash
# 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.

```js update-title.mjs
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.')
```

```bash
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:

```json
{ "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, `429` with
  `Retry-After`.
- Files: 256 KB each, 400 files, 5 MB of text. Media: images 3 MB, fonts
  1 MB, 100 files, 40 MB.
