# Zibox — API de Integração (llms.txt)

Zibox é uma plataforma de armários inteligentes (smart lockers) para condomínios brasileiros.
Esta API permite a desenvolvedores externos ler kiosks e apartamentos, alterar telefones e
senha de apartamentos, e receber eventos via webhooks — tudo dentro do escopo da conta do integrador.

- Spec OpenAPI (legível por máquina): `https://api.zibox.com.br/v1/openapi-integration.json`
- Referência navegável (Scalar): `https://api.zibox.com.br/reference/integration`

## Autenticação

Inclua `Authorization: Bearer zint_…` em **toda** requisição para `/v1/integration/*`.
A chave é criada pelo portal do integrador (ou pelo admin Zibox). O escopo da chave é ⊆ escopo
da conta; fora do escopo → 403. Chave revogada → 401.

```bash
curl -H "Authorization: Bearer zint_SEU_TOKEN" https://api.zibox.com.br/v1/integration/kiosks
```

## Convenções do Stack (críticas)

- **`kiosk_id` é texto humano** (ex: `ED_SKYHOUSE`, `CAMILA`), não UUID.
- **Telefones são BIGINT** no formato `55+DDD+9dígitos` (ex: `5511994826486`) — sem `+`,
  espaços, parênteses ou hífens. Envie apenas dígitos.
- **`senha` NUNCA retorna** em nenhuma resposta nem em payload de webhook. Apenas
  `senha_alterada: true` aparece quando a senha é alterada.
- **Multi-tenant**: tudo é escopado por `kiosk_id`; você enxerga apenas os kiosks da sua conta.
- **Sagas são assíncronas** (mutações viajam ao kiosk via SSE). Semântica de status:
  - `confirmed` — executado e confirmado pelo kiosk.
  - `queued` — kiosk offline; comando enfileirado e aplicado quando voltar (last-write-wins por campo).
  - `superseded` — LWW descartou (edição local mais nova venceu, contado como sucesso).
  - `timeout` — kiosk offline e não enfileirável; **NÃO executou**.
  - `failed` / `rolled_back` — kiosk rejeitou; leia `error` antes de retentar.
  - `expired` / `cancelled` — saiu da fila sem executar.

## Modo Sandbox / Teste

Toda conta de integrador tem um **armário fictício de teste**, isolado da frota real
(test mode estilo Stripe). Use-o para aprender e validar a integração ponta a ponta **sem
afetar produção**: os eventos de webhook têm payload e assinatura HMAC **idênticos** aos de
produção — só o armário é fictício.

- **Chaves de teste** têm prefixo `zint_test_…` (as de produção são `zint_…`). Crie na tela
  "Chaves API" do portal sandbox (`https://sandbox.zibox.com.br`) ou via API (abaixo).
- **Test e live são estritamente separados:** uma chave `zint_test_` só enxerga o armário
  fictício; nas rotas `/v1/integration/sandbox/*` uma chave live `zint_` é rejeitada (403).
- **Webhooks** criados no sandbox nascem `is_test` e recebem só eventos do armário fictício.
- **Operar em teste** = os MESMOS endpoints `/v1/integration/*` deste documento, só autenticando
  com a chave `zint_test_`. O kiosk de teste aparece em `GET /v1/integration/kiosks` como
  qualquer outro; a verificação de assinatura e o catálogo de eventos (seção Webhooks) valem igual.

### Disparar eventos no armário de teste

Em produção os eventos nascem do armário físico; no sandbox você os dispara — pela tela
**Simulador** do portal, ou por estas rotas (aceitam a sessão do portal **ou** uma chave `zint_test_`):

#### `POST /v1/integration/sandbox/simulate/entrega`
Registra uma entrega (ocupa uma porta livre do armário fictício) e emite `entrega.criada`.
```json
{ "id_apto": 42, "torre": "A", "tipo": "entrega" }
```

#### `POST /v1/integration/sandbox/simulate/retirada`
Retira uma entrega aberta e emite `retirada.criada`. Pegue um `entrega_id` válido em
`GET /v1/integration/sandbox/entregas`.
```json
{ "entrega_id": 1001 }
```

#### `POST /v1/integration/sandbox/simulate/kiosk-status`
Emite `kiosk.online` ou `kiosk.offline`.
```json
{ "online": true }
```

#### `GET /v1/integration/sandbox/entregas`
Lista entregas abertas (ainda ocupando porta) — alvos válidos para simular uma retirada.

#### `POST /v1/integration/sandbox/reset`
Restaura o armário de teste ao estado inicial (re-seed das portas/apartamentos fictícios).

#### `POST /v1/integration/sandbox/api-keys` · `POST /v1/integration/sandbox/webhooks`
Atalhos para criar uma chave `zint_test_` (body `{ "name": "…" }`) ou um webhook de teste
(body `{ "name", "url", "events": [...] }`, retorna `secret_once` uma vez) direto pela API.

## Endpoints

Todas as rotas abaixo ficam sob `https://api.zibox.com.br`.

---

### Kiosks

#### `GET /v1/integration/kiosks`
Lista os kiosks da sua conta.

```json
{
  "count": 2,
  "kiosks": [
    {
      "kiosk_id": "ED_SKYHOUSE",
      "nome": "Ed. Skyhouse",
      "online": true,
      "last_seen_at": "2026-06-12T14:00:00.000Z",
      "agent_version": "0.9.32"
    }
  ]
}
```

---

### Apartamentos

#### `GET /v1/integration/kiosks/:id/aptos`
Lista apartamentos de um kiosk. **Sem campo `senha_alterada` aqui** — isso só aparece em webhooks.

```json
{
  "count": 10,
  "apartamentos": [
    {
      "kiosk_id": "ED_SKYHOUSE",
      "id": 42,
      "torre": "A",
      "whatsapp1": 5511994826486,
      "whatsapp2": null,
      "whatsapp3": null,
      "mensagem_para_entregador": null,
      "updated_at": "2026-06-10T12:00:00.000Z"
    }
  ]
}
```

#### `GET /v1/integration/kiosks/:id/aptos/:torre/:aptoId`
Retorna um apartamento específico. Shape igual ao item acima (dentro de `{ "apto": { … } }`). Sem senha.

#### `PATCH /v1/integration/kiosks/:id/aptos/:torre/:aptoId`
**Única mutação de apartamento.** Campos aceitos no body (todos opcionais):

- `whatsapp1`, `whatsapp2`, `whatsapp3` — BIGINT (formato 55+DDD+9dígitos, ou `null` para limpar)
- `senha` — inteiro `0..2147483647` (o valor nunca retorna em nenhuma resposta)

Exemplo (apenas telefone):
```json
{
  "whatsapp1": 5511994826486,
  "whatsapp2": null
}
```

Observações:
- Envie apenas os campos que mudam.
- Telefones devem seguir o formato BIGINT (ver Convenções).

Respostas possíveis:

| Status | Significado | Body |
|--------|-------------|------|
| 200 | Kiosk online — executado | `{ "saga_id": "…", "status": "confirmed" | "superseded", "final_state": { … } }` |
| 202 | Kiosk offline elegível — enfileirado | `{ "saga_id": "…", "status": "queued" }` |
| 400 | Telefone ou senha inválidos | `{ "error": "…" }` |
| 429 | Fila cheia | `{ "error": "queue_full" }` |
| 502 | Kiosk offline e não enfileirável | `{ "error": "kiosk_offline", "queue_supported": false }` |

---

### Comandos / Sagas

#### `GET /v1/integration/commands?status=&kiosk_id=&limit=`
Lista comandos emitidos pela sua conta. Campos: `id, kiosk_id, command, status, error, params, result, created_at, issued_at, queued_until, completed_at`. Os campos `params` e `result` nunca expõem `senha`.

#### `GET /v1/integration/commands/:saga_id`
Retorna um comando pelo ID (mesmo shape, sem wrapper). 404 é anti-enumeração (pode ser fora do seu escopo).

#### `DELETE /v1/integration/commands/:saga_id`
Cancela um comando no estado `queued`.

```json
{ "saga_id": "cmd_…", "status": "cancelled" }
```

409 se o comando já saiu da fila (foi aplicado, expirou ou foi cancelado).

---

### Webhooks

#### `GET /v1/integration/webhooks`
Lista webhooks. `secret` **nunca retorna** após a criação.

```json
{
  "count": 1,
  "webhooks": [
    {
      "id": "wh_…",
      "name": "Meu Webhook",
      "url": "https://meuservidor.com/webhook",
      "events": ["entrega.criada", "apartamento.atualizado"],
      "active": true,
      "disabled_reason": null,
      "last_delivery_at": "2026-06-12T14:00:00.000Z",
      "last_delivery_status": 200,
      "created_at": "2026-06-01T00:00:00.000Z",
      "updated_at": "2026-06-12T14:00:00.000Z"
    }
  ]
}
```

#### `POST /v1/integration/webhooks`
Cria um webhook. A URL deve ser HTTPS público — IP privado/loopback é rejeitado (proteção SSRF).

Request:
```json
{
  "name": "Meu Webhook",
  "url": "https://meuservidor.com/webhook",
  "events": ["entrega.criada", "retirada.criada"]
}
```

Resposta 201 inclui `secret_once` com o `whsec_…` (mostrado **uma única vez** — guarde imediatamente).

#### `PATCH /v1/integration/webhooks/:id`
Atualiza nome, URL, eventos ou status (`active: true` reativa um webhook desabilitado).

Body aceita: `{ "name"?, "url"?, "events"?, "active"? }`

#### `DELETE /v1/integration/webhooks/:id`
Remove o webhook.

#### `POST /v1/integration/webhooks/:id/test`
Dispara um `teste.ping` síncrono para verificar se a URL está respondendo.

```json
{
  "delivered": true,
  "status_code": 200,
  "error": null,
  "duration_ms": 145
}
```

#### `GET /v1/integration/webhooks/:id/deliveries?limit=`
Histórico de tentativas de entrega (1 linha por tentativa).

```json
{
  "count": 5,
  "deliveries": [
    {
      "id": "del_…",
      "event_id": "evt_…",
      "event_type": "entrega.criada",
      "attempt": 1,
      "status_code": 200,
      "error": null,
      "duration_ms": 90,
      "created_at": "2026-06-12T14:00:00.000Z",
      "delivered_at": "2026-06-12T14:00:00.090Z",
      "next_retry_at": null
    }
  ]
}
```

`delivered_at` setado = entregue; `next_retry_at` setado = aguardando retry; ambos null = tentativas esgotadas.

#### `GET /v1/integration/event-types`
Catálogo completo de tipos de evento disponíveis para assinatura.

## Webhooks — Receber e Verificar

### Envelope

Todo POST enviado pelo Zibox tem este formato:

```json
{
  "id": "evt_abc123",
  "type": "<tipo-do-evento>",
  "kiosk_id": "ED_SKYHOUSE",
  "created_at": "2026-06-12T14:00:00.000Z",
  "data": { "…": "…" }
}
```

### Headers

```
Content-Type: application/json; charset=utf-8
User-Agent: Zibox-Webhooks/1.0
X-Zibox-Signature: t=<unix_segundos>,v1=<hmac_hex>
```

### Verificação de assinatura

**Fórmula:** `v1 = HMAC-SHA256(secret, `${t}.${corpo_cru}`)` em hex, onde `secret` = `whsec_…`
retornado na criação do webhook.

**CRÍTICO:** Verifique sobre o corpo **cru** recebido — não faça `JSON.parse` e re-serialize.
A ordem das chaves muda e o HMAC quebra. Rejeite se `t` estiver fora de ±5 minutos (anti-replay).

```js
import crypto from 'node:crypto'
// rawBody = corpo EXATO recebido (Buffer/string), sem reparsear
function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(kv => kv.split('=')))
  const t = parts.t, v1 = parts.v1
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false // > 5 min
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
  return crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
}
```

### Semântica de entrega

- **At-least-once** → deduplique pelo `id` do envelope.
- Responda `2xx` em até **10 segundos**; qualquer outra coisa agenda retry com backoff
  (~1 min → 5 min → 30 min → 2 h → 6 h, janela de ~24 h).
- Webhook que falha por 3 dias consecutivos é **auto-desativado** (`active: false`,
  `disabled_reason: "auto_disabled_failures"`). Reative com `PATCH { "active": true }`.

---

### Catálogo de eventos subscritíveis

Os 8 eventos abaixo podem ser incluídos no array `events` ao criar/editar um webhook.
O evento `teste.ping` é disparado apenas pelo endpoint de teste e não é assinável.

---

#### `entrega.criada`
Nova entrega registrada no armário.

```json
{
  "id": "evt_…",
  "type": "entrega.criada",
  "kiosk_id": "ED_SKYHOUSE",
  "created_at": "2026-06-12T14:00:00.000Z",
  "data": {
    "id": 1001,
    "id_apto": 42,
    "torre": "A",
    "tipo": "entrega",
    "numero_porta": 7,
    "created_at": "2026-06-12T14:00:00.000Z"
  }
}
```

---

#### `retirada.criada`
Retirada de pacote registrada no armário.

```json
{
  "data": {
    "id": 2001,
    "entrega_id": 1001,
    "id_apto": 42,
    "torre": "A",
    "tipo": "retirada",
    "numero_porta": 7,
    "created_at": "2026-06-12T14:10:00.000Z"
  }
}
```

---

#### `apartamento.criado`
Apartamento adicionado ao kiosk.

```json
{
  "data": {
    "id": 42,
    "torre": "A",
    "whatsapp1": 5511994826486,
    "whatsapp2": null,
    "whatsapp3": null
  }
}
```

---

#### `apartamento.atualizado`
Apartamento alterado (telefones, senha ou mensagem).
`changed_fields` é a lista crua dos NOMES dos campos alterados e PODE conter a string
`"senha"` (é só o nome do campo, seguro) — junto com `whatsapp1/2/3` e
`mensagem_para_entregador`. Quando `"senha"` está em `changed_fields`, vem também
`senha_alterada: true`. O VALOR da senha nunca aparece em lugar nenhum do payload.

```json
{
  "data": {
    "id": 42,
    "torre": "A",
    "changed_fields": ["whatsapp2", "senha"],
    "whatsapp1": 5511994826486,
    "whatsapp2": 5511988887777,
    "whatsapp3": null,
    "senha_alterada": true
  }
}
```

---

#### `apartamento.excluido`
Apartamento removido do kiosk.

```json
{
  "data": {
    "id": 42,
    "torre": "A"
  }
}
```

---

#### `comando.atualizado`
Comando de integração/enfileirado mudou de status (terminal ou queued).

```json
{
  "data": {
    "saga_id": "f593b867-4268-4a4b-a1b2-96f9e1566838",
    "command": "update_apartamento",
    "status": "confirmed",
    "applied_fields": ["whatsapp2"],
    "superseded_fields": [],
    "error": null
  }
}
```

---

#### `kiosk.online`
Kiosk conectou via SSE. Pode oscilar em reconexões — considere debounce no consumidor.

```json
{
  "data": {
    "kiosk_id": "ED_SKYHOUSE",
    "connected_at": "2026-06-12T14:00:00.000Z"
  }
}
```

---

#### `kiosk.offline`
Kiosk desconectou via SSE. Pode oscilar em reconexões — considere debounce no consumidor.

```json
{
  "data": {
    "kiosk_id": "ED_SKYHOUSE",
    "last_seen_at": "2026-06-12T14:05:00.000Z"
  }
}
```

---

### Exemplo completo de entrega — `entrega.criada`

```
POST https://meuservidor.com/webhook HTTP/1.1
Content-Type: application/json; charset=utf-8
User-Agent: Zibox-Webhooks/1.0
X-Zibox-Signature: t=1749730800,v1=a3f8c...2d1e

{
  "id": "evt_abc123",
  "type": "entrega.criada",
  "kiosk_id": "ED_SKYHOUSE",
  "created_at": "2026-06-12T14:00:00.000Z",
  "data": {
    "id": 1001,
    "id_apto": 42,
    "torre": "A",
    "tipo": "entrega",
    "numero_porta": 7,
    "created_at": "2026-06-12T14:00:00.000Z"
  }
}
```

## Erros

| Status HTTP | Significado |
|-------------|-------------|
| 400 | Validação — campo inválido (telefone fora do formato, senha fora do range, URL de webhook privada) |
| 401 | Sem chave ou chave inválida/revogada |
| 403 | Fora do escopo da conta |
| 404 | Não encontrado (ou anti-enumeração — recurso pode existir mas fora do escopo) |
| 409 | Conflito — ex: cancelar comando que já saiu da fila |
| 429 | Fila de comandos cheia |
| 502 | Kiosk offline e não enfileirável para este tipo de comando |

## Informações do Contrato

- **Versão do contrato:** 1.0 (2026-06)
- **Fonte canônica:** O spec OpenAPI em `https://api.zibox.com.br/v1/openapi-integration.json` é a referência
  definitiva para rotas, parâmetros e schemas. A seção `webhooks` do spec documenta os
  payloads de cada evento com schemas JSON Schema completos.
- Este documento (`llms.txt`) é gerado automaticamente e sincronizado com o código do servidor.