# API del tema

`https://theme.hystore.com.br/v1` lee y cambia el tema en código de una tienda.
Es la API que usa la herramienta hystore, abierta para tus integraciones y
para CI. La lista completa de rutas, con sus formatos, está en la
[OpenAPI](/openapi.json).

## 1. Crea una llave

1. En el panel, abre **Personalización > Acceso al tema > Crear llave**.
2. Dale un nombre (por ejemplo, *Deploy del sitio*).
3. Elige lo que puede hacer:

| Alcance | Permite |
|---|---|
| `theme:read` | leer archivos, versiones y medios; revisar; enlaces de vista previa |
| `theme:write` | cambiar el borrador, imágenes y fuentes; traer una versión de vuelta |
| `theme:publish-request` | pedir publicación (se aprueba en el panel) |

4. Elige cuánto dura (30 a 365 días) y, si quieres, las IP que pueden usarla.
5. Copia la llave (`hys_theme_...`). Aparece **una sola vez**: guárdala como una
   contraseña, en un baúl o en los secretos del CI.

> [!WARNING]
> La llave nunca va en el navegador ni en un repositorio. La API rechaza
> pedidos de páginas web (con encabezado `Origin`): llámala desde servidores, CI
> y herramientas.

## 2. Prueba la llave

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

La tienda es siempre la de la llave: ninguna ruta recibe id de tienda. Una
llave nunca hace más de lo que permite el rol de quien la creó, y revocarla en
el panel la corta al instante.

## Leer el tema

```bash
# Archivos del borrador, con sha256 y tamaño, y la revisión del borrador
curl https://theme.hystore.com.br/v1/theme/files -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

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

# Todos los archivos de texto de una vez (con contenido y hash)
curl https://theme.hystore.com.br/v1/theme/export -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
```

Un archivo viene así:

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

## Escribir con seguridad

Devuelve el `sha256` que leíste 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..."}'
```

- Si el archivo cambió desde que lo leíste, la respuesta es `409` con el hash
  actual, y no se escribe nada. Lee de nuevo, junta y escribe.
- `baseSha: null` quiere decir que el archivo todavía no puede existir (crear
  sin borrar el de otra persona).
- Sin `baseSha`, la escritura pisa lo que haya.

Varios archivos de una vez, todos o ninguno (hasta 100 cambios):

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

`content: null` borra. `expectedRevision` (opcional) rechaza la escritura si
otra pasó antes desde tu lectura.

Un archivo que rompe una regla recibe `422` con 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 }] }
```

## Revisar, ver la vista previa y publicar

```bash
# Revisa el borrador entero
curl -X POST https://theme.hystore.com.br/v1/theme/check -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Enlace de 24 horas que abre el borrador en la dirección de la tienda
curl -X POST https://theme.hystore.com.br/v1/theme/preview -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Pide la publicación (alguien aprueba en el panel)
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 de Black Friday"}'
```

## Versiones

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

# Un archivo como estaba en la versión 3
curl "https://theme.hystore.com.br/v1/theme/versions/3/file?path=styles/theme.css" -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Trae la versión 3 de vuelta al borrador (la tienda en el aire no cambia)
curl -X POST https://theme.hystore.com.br/v1/theme/versions/3/restore -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"
```

## Imágenes y fuentes

```bash
# Las lista, con la dirección pública de cada una
curl https://theme.hystore.com.br/v1/theme/assets -H "Authorization: Bearer $HYSTORE_THEME_TOKEN"

# Envía (el cuerpo es el archivo; el tipo se lee del contenido)
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

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

Imágenes PNG, JPEG, WebP, GIF y AVIF hasta 3 MB; fuentes WOFF y WOFF2 hasta
1 MB. SVG no entra como archivo de medios.

## Ejemplo en Node.js

Cambia un texto del tema y pide la publicación. Funciona en Node.js 18 o más
nuevo, sin bibliotecas.

```js actualizar-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. Lee el archivo de textos en español
const file = await call('GET', '/theme/file?path=locales/es.json')
const texts = JSON.parse(file.content)

// 2. Cambia el título y escribe, con el hash leído (409 si alguien lo cambió antes)
texts.home = { ...texts.home, title: 'Llegó la temporada 5' }
await call('PUT', '/theme/file?path=locales/es.json', { content: JSON.stringify(texts, null, 2), baseSha: file.sha256 })

// 3. Pide la publicación
await call('POST', '/theme/publish-requests', { note: 'Título de la temporada 5' })
console.log('Listo: apruébalo en Personalización > Tema en código.')
```

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

## Rutas

| Ruta | Alcance |
|---|---|
| `GET /v1/me` | cualquiera |
| `GET /v1/theme`, `GET /v1/theme/files`, `GET /v1/theme/file?path=` | read |
| `GET /v1/theme/export` | read |
| `PUT` y `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` y `DELETE /v1/theme/asset?path=` | read, write |
| `POST /v1/theme/publish-requests` | publish-request |

## Respuestas de error

Todo error tiene el mismo formato y nunca trae detalles internos:

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

| Estado | Quiere decir |
|---|---|
| `400` | pedido mal formado (ruta, cuerpo) |
| `401` | sin llave, llave equivocada, vencida o revocada |
| `403` | la llave no tiene el alcance, la IP no está en la lista, el plan no tiene el módulo, o el pedido vino de una página web |
| `404` | el archivo o la versión no existe |
| `409` | el archivo o el borrador cambió desde tu lectura |
| `413` | archivo o lote demasiado grande |
| `422` | el archivo rompe una regla del tema (con la línea) |
| `429` | demasiadas llamadas; espera los segundos de `Retry-After` |

## Límites

- 120 lecturas y 30 escrituras por minuto por llave; arriba de eso, `429` con
  `Retry-After`.
- Archivos: 256 KB cada uno, 400 archivos, 5 MB de texto. Medios: imágenes
  3 MB, fuentes 1 MB, 100 archivos, 40 MB.
