<!--
ARQUIVO-FATO (constituição, Duas fontes da verdade) — o heartbeat do integrador-client (contrato K1 do plano 017).
Publicado cru em https://docs.xadm.biz/aplicacoes/integrador-server/raw/heartbeat.md.
O integrador-client LINKA este arquivo (URL absoluta) — não o copia.

Fragmento: sem frontmatter, headings a partir de ### (entra sob um ## da página
hospedeira). Os fatos abaixo saem do código deste repo (heartbeat/HeartbeatController,
HeartbeatEntrada, HeartbeatRepasse), nunca de memória. Mudou algo aqui: é mudança de
contrato entre repos — client, este server e o central mudam na mesma leva, com as
fixtures de teste (src/test/resources/contrato/).
-->

### Heartbeat: o integrador-client avisa que está vivo

```
POST /api/v1/heartbeat
```

Cada instalação do integrador-client manda um heartbeat no startup e a cada 60 min (versão, fluxos
ativos, hostname). Este integrador-server **carimba o `cliente_id`** da instância (env `CLIENTE_ID`) e
**repassa** ao central-backend em `POST {CENTRAL_URL}/api/integrador/heartbeat`, com o Bearer
`CENTRAL_API_TOKEN`. O que o central grava, o alerta de silêncio e a lista de instalações são do
central: contrato do repasse na
[API REST do central-backend](https://docs.xadm.biz/aplicacoes/central-backend/dev/api-rest/) e o
porquê na [decisão local 0028 do central](https://docs.xadm.biz/aplicacoes/central-backend/decisoes/0028-heartbeat-integrador-client/).

- **URL:** esquema + host + porta de `powersync.upload.url`, com o path `/api/v1/heartbeat`; ou
  `heartbeat.url` quando definida.
- **Headers:** `Authorization: Bearer <powersync.upload.token>` (é o `INTEGRADOR_API_TOKEN` da
  instância) e `Content-Type: application/json`.

Amostra canônica **A**:

```json
{"versao":"0.2.0","fluxos":["pied"],"hostname":"SRV-ERP01","motivo":"inicio","iniciado_em":"2026-09-10T08:00:03-03:00"}
```

| Campo | Tipo no fio | Regra (validada pelo integrador-server) |
|---|---|---|
| `versao` | string | obrigatório, 1–40 caracteres |
| `fluxos` | array de string | sempre enviado pelo client; ≤ 20 itens, cada um `^[a-z0-9-]{1,50}$` (os nomes de `Fluxo`: `pied`, `abastecimento`, `encerra-mdfe`) |
| `hostname` | string ou `null` | ≤ 255; `null` quando o client não consegue resolver |
| `motivo` | string | `inicio` \| `periodico` |
| `iniciado_em` | string | `DateTimeFormatter.ISO_OFFSET_DATE_TIME` sobre um `OffsetDateTime` truncado a segundos, no offset local da máquina; offset zero sai `Z` (ex. no CI em UTC). Sempre com segundos (o `toString()` do Java os omite quando são zero, por isso não serve). **Não** validado pelo integrador-server (repassa opaco) |

Respostas (corpo problem+json; o client só lê o status):

| Status | `code` | Quando |
|---|---|---|
| `204` | — | central aceitou |
| `400` | `BAD_REQUEST` | viola a tabela acima (`versao`, `hostname`, `motivo`, `fluxos`) |
| `401` | — | Bearer ausente ou errado (`ApiBearerSecurityRule`) |
| `502` | `heartbeat_recusado` | central devolveu 4xx (inclusive corpo que não é problem+json) |
| `502` | `central_indisponivel` | central 5xx, timeout ou rede |
| `503` | `heartbeat_desligado` | `CENTRAL_API_TOKEN` ou `CLIENTE_ID` vazio no integrador-server |

**Repasse sem transformação.** `versao`, `fluxos`, `hostname`, `motivo` e `iniciado_em` seguem ao
central como vieram (`iniciado_em` é string opaca — quem a valida é o central); só entra o
`cliente_id`. Campo ausente vai como `null`, `fluxos` ausente vai como `[]`, e campo que este contrato
não conhece é **ignorado** e não é repassado — é o que deixa o client ganhar campo sem quebrar o server.
