# API do tema

`https://theme.hystore.com.br/v1` lê e muda o tema em código de uma loja. É a
API que a ferramenta hystore usa, aberta para as suas integrações e para CI. A
lista completa de rotas, com os formatos, está na [OpenAPI](/openapi.json).

## 1. Crie uma chave

1. No painel, abra **Personalização > Acesso ao tema > Criar chave**.
2. Dê um nome (por exemplo, *Deploy do site*).
3. Escolha o que ela pode fazer:

| Escopo | Permite |
|---|---|
| `theme:read` | ler arquivos, versões e mídia; conferir; links de prévia |
| `theme:write` | mudar o rascunho, imagens e fontes; trazer uma versão de volta |
| `theme:publish-request` | pedir publicação (aprovada no painel) |

4. Escolha a validade (30 a 365 dias) e, se quiser, os IPs que podem usá-la.
5. Copie a chave (`hys_theme_...`). Ela aparece **uma vez só**: guarde como uma
   senha, num cofre ou nos segredos do CI.

> [!WARNING]
> A chave nunca vai no navegador nem num repositório. A API recusa pedidos
> vindos de páginas web (com cabeçalho `Origin`): chame de servidores, CI e
> ferramentas.

## 2. Teste a chave

```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": "Minha Loja", "domain": "minhaloja.hystore.com.br" },
  "scopes": ["theme:read", "theme:write"],
  "credential": { "id": "...", "kind": "key" }
}
```

A loja é sempre a da chave: nenhuma rota recebe id de loja. Uma chave nunca
faz mais do que o cargo de quem a criou permite, e revogar no painel corta na
hora.

## Ler o tema

```bash
# Arquivos do rascunho, com sha256 e tamanho, e a revisão do rascunho
curl https://theme.hystore.com.br/v1/theme/files -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Um arquivo
curl "https://theme.hystore.com.br/v1/theme/file?path=sections/hero.liquid" -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Todos os arquivos de texto de uma vez (com conteúdo e hash)
curl https://theme.hystore.com.br/v1/theme/export -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
```

Um arquivo vem assim:

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

## Gravar com segurança

Mande de volta o `sha256` que você leu como `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\":\"Título\"}]}{% endschema %}","baseSha":"3f1c..."}'
```

- Se o arquivo mudou desde a sua leitura, a resposta é `409` com o hash atual,
  e nada é gravado. Leia de novo, junte e grave.
- `baseSha: null` quer dizer que o arquivo ainda não pode existir (criar sem
  apagar o de alguém).
- Sem `baseSha`, a gravação passa por cima.

Vários arquivos de uma vez, todos ou nenhum (até 100 mudanças):

```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\":\"Bem-vindo\"}}","baseSha":"9a0b..."},{"path":"sections/old.liquid","content":null}],"expectedRevision":12}'
```

`content: null` apaga. `expectedRevision` (opcional) recusa a gravação se outra
passou na frente desde a sua leitura.

Um arquivo que quebra uma regra recebe `422` com cada problema:

```json
{ "errors": [{ "code": "html_event_handler", "message": "Event attributes (onclick, onload...) are not allowed. Use data-hystore-* components.", "path": "sections/hero.liquid", "line": 3 }] }
```

## Conferir, ver a prévia e publicar

```bash
# Confere o rascunho inteiro
curl -X POST https://theme.hystore.com.br/v1/theme/check -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Link de 24 horas que abre o rascunho no endereço da loja
curl -X POST https://theme.hystore.com.br/v1/theme/preview -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Pede a publicação (alguém aprova no painel)
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":"Banner da Black Friday"}'
```

## Versões

```bash
# As últimas 30 versões publicadas
curl https://theme.hystore.com.br/v1/theme/versions -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Um arquivo como estava na versão 3
curl "https://theme.hystore.com.br/v1/theme/versions/3/file?path=styles/theme.css" -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Traz a versão 3 de volta para o rascunho (a loja no ar não muda)
curl -X POST https://theme.hystore.com.br/v1/theme/versions/3/restore -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
```

## Imagens e fontes

```bash
# Lista, com o endereço público de cada uma
curl https://theme.hystore.com.br/v1/theme/assets -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Envia (o corpo é o arquivo; o tipo é lido do conteúdo)
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

# Apaga
curl -X DELETE "https://theme.hystore.com.br/v1/theme/asset?path=assets/banner.webp" -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
```

Imagens PNG, JPEG, WebP, GIF e AVIF até 3 MB; fontes WOFF e WOFF2 até 1 MB.
SVG não entra como arquivo de mídia.

## Exemplo em Node.js

Troca um texto do tema e pede a publicação. Funciona no Node.js 18 ou mais novo,
sem bibliotecas.

```js atualizar-titulo.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. Lê o arquivo de textos em português
const file = await call('GET', '/theme/file?path=locales/pt.json')
const texts = JSON.parse(file.content)

// 2. Muda o título e grava, com o hash lido (409 se alguém mudou antes)
texts.home = { ...texts.home, title: 'Temporada 5 chegou' }
await call('PUT', '/theme/file?path=locales/pt.json', { content: JSON.stringify(texts, null, 2), baseSha: file.sha256 })

// 3. Pede a publicação
await call('POST', '/theme/publish-requests', { note: 'Título da temporada 5' })
console.log('Pronto: aprove em Personalização > Tema em código.')
```

```bash
HYSTORE_THEME_TOKEN=hys_theme_... node atualizar-titulo.mjs
```

## Rotas

| Rota | Escopo |
|---|---|
| `GET /v1/me` | qualquer |
| `GET /v1/theme`, `GET /v1/theme/files`, `GET /v1/theme/file?path=` | read |
| `GET /v1/theme/export` | read |
| `PUT` e `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` e `DELETE /v1/theme/asset?path=` | read, write |
| `POST /v1/theme/publish-requests` | publish-request |

## Respostas de erro

Todo erro tem o mesmo formato e nunca traz detalhe interno:

```json
{ "errors": [{ "code": "SCOPE_REQUIRED", "message": "This credential cannot do this (needs theme:write)." }] }
```

| Status | Quer dizer |
|---|---|
| `400` | pedido mal formado (caminho, corpo) |
| `401` | sem chave, chave errada, vencida ou revogada |
| `403` | a chave não tem o escopo, o IP não está na lista, o plano não tem o módulo, ou o pedido veio de uma página web |
| `404` | o arquivo ou a versão não existe |
| `409` | o arquivo ou o rascunho mudou desde a sua leitura |
| `413` | arquivo ou lote grande demais |
| `422` | o arquivo quebra uma regra do tema (com a linha) |
| `429` | muitas chamadas; espere os segundos de `Retry-After` |

## Limites

- 120 leituras e 30 escritas por minuto por chave; acima disso, `429` com
  `Retry-After`.
- Arquivos: 256 KB cada, 400 arquivos, 5 MB de texto. Mídia: imagens 3 MB,
  fontes 1 MB, 100 arquivos, 40 MB.
