API A91 — captação & fornecimento

Como os aparelhos e apps enviam dados de saúde para a plataforma, e como o app do paciente e o profissional leem esses dados — sempre via Bearer token e respeitando o consentimento.

⚛️ React
⚙️ PHP 8
REST · JSON
🔒 Bearer (JWT)

Visão geral

API REST sobre HTTPS; corpo e respostas em JSON (Content-Type: application/json). Toda chamada exige autenticação por Bearer token.

AmbienteBase URL
Produçãohttps://api.a91.health/v1
Sandboxhttps://sandbox.a91.health/v1
Há dois grupos de endpoints: Captação (escrita — aparelhos/app enviam medições) e Fornecimento (leitura — app e profissional consultam). O acesso de leitura por um profissional é sempre filtrado pelo consentimento do paciente (RLS no banco).

Autenticação

A API usa Bearer token (JWT). O cliente troca suas credenciais por um access_token de curta duração e o envia no cabeçalho Authorization de toda requisição.

1. Obter o token

POST/auth/token

Fluxo client credentials para dispositivos/serviços, ou password para login de usuário/profissional no app.

Requisição
POST /v1/auth/token
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "dev_anel_a91",
  "client_secret": "••••••••••••",
  "scope": "dados:escrever"
}
Resposta 200
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "dados:escrever",
  "refresh_token": "rt_9f2c…"
}

2. Usar o token

Inclua o token em todas as chamadas:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6...

Escopos

EscopoPermiteQuem usa
dados:escreverEnviar medições (captação)Anel, pulseira, balança, app
saude:lerLer resumo/leituras/bioimpedânciaApp do paciente, profissional
comunicacaoMensagens e planosApp, profissional
Expiração & refresh: o access_token dura 1h. Use o refresh_token em POST /auth/token com grant_type=refresh_token. Token ausente/expirado → 401; token válido mas sem escopo/consentimento → 403.

Convenções

TemaRegra
IDsUUID v4. usuario_id é o pseudônimo — PII nunca trafega nestes endpoints.
DatasISO-8601 em UTC: 2026-05-24T08:12:00Z
Unidadesbpm, %, °C, passos, minutos (sono), kg.
IdempotênciaEnvios aceitam header Idempotency-Key; reenvio com a mesma chave não duplica.
Paginação?limit= (máx 1000) + ?cursor=; resposta traz proxima_pagina.
Rate limitHeaders X-RateLimit-Remaining / -Reset. Estouro → 429.

Captação · Leituras

POST/ingest/leiturasdados:escrever

Envia um lote de medições do anel/pulseira (steps, heart_rate, spo2, temperature, sleep, battery). Aceita batch para economizar bateria/rede.

Campos (cada item de leituras[])
CampoTipoDescrição
usuario_id obriguuidPseudônimo do paciente.
equipamento_id obriguuidAparelho de origem.
tipo_dispositivo obrigenumanel · pulseira · balanca
metrica obrigenumsteps · heart_rate · spo2 · temperature · sleep · battery
valor obrignumberValor medido.
unidadestringbpm, %, °C…
ts obrigdatetimeMomento da medição (UTC).
payloadobjectBruto adicional do aparelho (opcional).
Requisição
POST /v1/ingest/leituras
Authorization: Bearer <token>
Idempotency-Key: a1f3c-2026-05-24-08

{
  "leituras": [
    {
      "usuario_id": "usr_8a1f3c",
      "equipamento_id": "eqp_anel_03",
      "tipo_dispositivo": "anel",
      "metrica": "heart_rate",
      "valor": 62,
      "unidade": "bpm",
      "ts": "2026-05-24T08:12:00Z"
    },
    {
      "usuario_id": "usr_8a1f3c",
      "equipamento_id": "eqp_pulseira_01",
      "tipo_dispositivo": "pulseira",
      "metrica": "steps",
      "valor": 8432,
      "ts": "2026-05-24T08:00:00Z"
    }
  ]
}
Resposta 202
{
  "recebidas": 2,
  "ignoradas_duplicadas": 0,
  "lote_id": "ing_77c1"
}

Captação · Bioimpedância

POST/ingest/bioimpedanciadados:escrever

Um envio por pesagem. A balança manda muitos campos — vão todos dentro de dados (objeto livre/JSONB).

Requisição
POST /v1/ingest/bioimpedancia
Authorization: Bearer <token>

{
  "usuario_id": "usr_8a1f3c",
  "equipamento_id": "eqp_bal_01",
  "ts": "2026-05-22T07:30:00Z",
  "dados": {
    "peso": 78.5, "imc": 23.4,
    "gordura_pct": 18.2, "musculo_kg": 38.5,
    "agua_pct": 56, "proteina_pct": 17,
    "gordura_visceral": 7, "massa_ossea": 3.2,
    "metabolismo_basal": 1720, "idade_metabolica": 33
  }
}
Resposta 201
{ "id": "bio_5d20", "status": "armazenado" }

Captação · Eventos do dispositivo

POST/ingest/eventosdados:escrever

Bateria, sincronização e estado de conexão de cada aparelho.

Requisição
POST /v1/ingest/eventos
Authorization: Bearer <token>

{
  "equipamento_id": "eqp_bal_01",
  "usuario_id": "usr_8a1f3c",
  "tipo": "conexao",   // sync | bateria | conexao
  "estado": "offline",
  "bateria": 60,
  "ts": "2026-05-22T07:31:00Z"
}
Resposta 202
{ "status": "ok" }

Fornecimento · Resumo do dia

GET/usuarios/{usuario_id}/resumosaude:ler

O que o dashboard mostra: 1 valor por métrica (a fonte é escolhida pela regra de negócio) + a idade fisiológica.

ParâmetroTipoDescrição
datadate (query)Dia desejado (default: hoje).
Requisição
GET /v1/usuarios/usr_8a1f3c/resumo?data=2026-05-24
Authorization: Bearer <token>
Resposta 200
{
  "usuario_id": "usr_8a1f3c",
  "data": "2026-05-24",
  "idade_fisiologica": 33,
  "metricas": {
    "passos":      { "valor": 8432, "fonte": "pulseira" },
    "heart_rate":  { "valor": 62,   "fonte": "anel" },
    "spo2":        { "valor": 98,   "fonte": "anel" },
    "temperatura": { "valor": 36.4, "fonte": "pulseira" },
    "sono":        { "valor": "7h24", "fonte": "anel" }
  }
}
Se quem chama é um profissional sem consentimento ativo no escopo, a resposta vem 403 — a RLS no banco simplesmente não retorna as linhas.

Fornecimento · Série de leituras

GET/usuarios/{usuario_id}/leiturassaude:ler

Série temporal de uma métrica num intervalo (ex.: batimento ao longo do dia). Paginada.

ParâmetroTipoDescrição
metrica obrigenum (query)heart_rate, steps…
from / todatetime (query)Janela de tempo (UTC).
limit / cursorint / stringPaginação.
Requisição
GET /v1/usuarios/usr_8a1f3c/leituras?metrica=heart_rate
    &from=2026-05-24T00:00:00Z&to=2026-05-24T23:59:59Z&limit=500
Authorization: Bearer <token>
Resposta 200
{
  "metrica": "heart_rate",
  "unidade": "bpm",
  "dados": [
    { "ts": "2026-05-24T08:00:00Z", "valor": 61 },
    { "ts": "2026-05-24T08:01:00Z", "valor": 62 }
  ],
  "proxima_pagina": "cursor_x9f2"
}

Fornecimento · Bioimpedância

GET/usuarios/{usuario_id}/bioimpedanciasaude:ler

Última pesagem (ou histórico com ?historico=true).

Resposta 200
{
  "ts": "2026-05-22T07:30:00Z",
  "dados": {
    "peso": 78.5, "imc": 23.4,
    "gordura_pct": 18.2, "musculo_kg": 38.5,
    "idade_metabolica": 33
  }
}

Fornecimento · Dispositivos

GET/usuarios/{usuario_id}/dispositivossaude:ler

Aparelhos pareados, com bateria e estado de conexão (o app mostra "2 de 3 conectados").

Resposta 200
{
  "dispositivos": [
    { "equipamento_id": "eqp_anel_03", "tipo": "anel",
      "conectado": true,  "bateria": 82, "ultima_sync": "2026-05-24T08:12:00Z" },
    { "equipamento_id": "eqp_pulseira_01", "tipo": "pulseira",
      "conectado": true,  "bateria": 47, "ultima_sync": "2026-05-24T08:09:00Z" },
    { "equipamento_id": "eqp_bal_01", "tipo": "balanca",
      "conectado": false, "bateria": 60, "ultima_sync": "2026-05-22T07:31:00Z" }
  ]
}

Códigos de erro

Erros seguem um corpo padrão:

{ "erro": "sem_consentimento", "mensagem": "Sem consentimento ativo no escopo 'cardio'." }
HTTPCódigoQuando
400requisicao_invalidaJSON malformado ou campo faltando.
401nao_autenticadoBearer token ausente, inválido ou expirado.
403sem_consentimento / sem_escopoToken válido, mas sem permissão/consentimento para o dado.
404nao_encontradoUsuário ou recurso inexistente.
409conflito_idempotenciaIdempotency-Key reusada com corpo diferente.
422dados_invalidosValor fora do domínio (ex.: métrica desconhecida).
429limite_excedidoRate limit. Tente após X-RateLimit-Reset.
5xxerro_internoFalha no servidor. Reenvio seguro com Idempotency-Key.
Privacidade: nenhum endpoint retorna PII (nome, CPF, e-mail) — só o pseudônimo usuario_id. A reidentificação acontece em um serviço separado, com cofre e auditoria próprios.