Referência detalhada

Especificação completa das 29 operações da v1.0 em português

Referência detalhada de endpoints

Especificação completa das 30 operações da v1.0 em português: saúde, pacientes (leitura + escrita), exames (leitura + escrita), laudos (leitura + ingestão), prontuários do paciente (leitura + escrita), agenda (leitura + escrita) e catálogos (procedimentos, convênios, médicos, CIDs). Todos os POST/PATCH (agenda, paciente, exame, ingestão de laudo e prontuário) compartilham o mesmo contrato descrito nas seções Idempotência e LGPD em POSTs e PATCHs — leitura recomendada antes de integrar escrita. Para experimentar interativamente, use o Swagger UI; para a visão de pássaro escaneável, abra o resumo.

Comum a todos os endpoints

Headers obrigatórios

Toda requisição autenticada deve enviar os três headers abaixo. Ver detalhes em autenticação e receitas por linguagem em Bash, Python, Node.js ou PHP.

HeaderFormatoNotas
X-API-Key svp_live_[0-9a-f]{32} Identificador público da chave. Pode aparecer em logs.
X-Timestamp Unix epoch (segundos) Janela ±300s vs. relógio do servidor. Use NTP. Inteiro estrito.
X-Signature HMAC-SHA256 hex lowercase (64 chars) Assina timestamp + "." + METHOD + " " + path_q + "\n" + body
Content-Type application/json Obrigatório quando houver body (POST); opcional em GET sem body.

Envelope padrão de resposta

{
  "data":  <objeto, array ou null>,
  "meta":  {
    "request_id": "<hex>",
    "duracao_ms": 12
    /* listagens: limit, offset, total, has_more */
  },
  "error": <objeto Error ou null>
}

Em erro, data é sempre null e error traz code / message / details. O catálogo completo de códigos está em códigos de erro.

Headers padronizados em toda resposta

X-Api-Version: v1
X-Api-Release: 1.1.0
X-Content-Type-Options: nosniff
Cache-Control: no-store

X-Api-Version: v1 identifica o contrato (inalterado). X-Api-Release é a versão semântica do gateway (atual 1.1.0, também em meta.version) e evolui de forma aditiva — o bump 1.1.0 introduziu a resolução por identificadores de negócio (aliases), os defaults por tenant no POST /v1/exames e o endpoint GET /v1/config, sem quebrar payloads existentes.

Latência típica: a 1ª chamada após inatividade prolongada (cold call) leva ~2300ms; chamadas subsequentes dentro de 5 min respondem em <800ms (cache de Authorization). Configure timeout mínimo de 5 segundos. Ver autenticação §5.

Idempotência — regra única de POSTs e PATCHs

Os 3 endpoints de escrita de agenda (criar / cancelar / reagendar) e os 2 endpoints de escrita de paciente (POST /v1/pacientes + PATCH /v1/pacientes/{id} — Fase 3*) exigem o header Idempotency-Key. Garante uma única operação por chave mesmo sob retry agressivo, race condition de rede ou cliente impaciente. Contrato é o mesmo entre Agenda e Pacientes — não há diferenças de comportamento.

Contrato

AtributoValor
Header obrigatórioIdempotency-Key
Formato1–64 chars, regex ^[A-Za-z0-9_-]+$
RecomendaçãoUUID v4 (uuidgen, uuid.uuid4(), crypto.randomUUID()) — uma key por requisição lógica, gerada no cliente
Janela de validade (TTL)24 horas a partir do 1º uso
EscopoPor chave de API — a mesma key em chaves distintas não colide
ComportamentoMesma key + mesmo body → resposta cacheada (replay); mesma key + body diferente → 422 IDEMPOTENCY_CONFLICT

Status original preservado no replay

Se o 1º POST devolveu 201 Created, o replay devolve 201 (não 200). O cliente distingue replay vs. original via header Idempotency-Replay: true | false e via meta.idempotent_replay.

Headers de resposta sempre presentes em POSTs

Idempotency-Key: 2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f
Idempotency-Replay: false
X-Api-Version: v1
X-Request-Id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
Location: /v1/agenda/agendamentos/8421   ; apenas no 201 de criar agendamento
Location: /v1/pacientes/4823             ; apenas no 201 de criar paciente
Retry-After: 2                           ; apenas no 409 CONCURRENT_REQUEST

Erros relacionados

  • 400 VAL_INVALID_PARAM — header ausente ou formato inválido.
  • 409 CONCURRENT_REQUEST — outra request em flight com a mesma key. Aguarde Retry-After segundos e tente novamente.
  • 422 IDEMPOTENCY_CONFLICT — key reusada com body distinto. Gere uma key nova ou envie o body idêntico ao original.

Network error é cacheado como 503

Quando o gateway perde a conexão com o backend depois de eventualmente já ter enviado a requisição, a resposta 503 é gravada no idempotency store em vez de a key ser apagada. Consequência prática: retry com a mesma Idempotency-Key devolve replay de 503 (não re-executa) — garante que um único agendamento seja criado mesmo se o backend processou e a resposta morreu na rede.

Para tentar novamente em cenário de network error, o cliente deve:

  1. Verificar via GET /v1/agenda/agendamentos?paciente_id=X se o registro foi criado mesmo assim; ou
  2. Gerar uma nova Idempotency-Key e refazer o POST.

A key fica queimada por 24h (TTL). É o trade-off correto: cliente paga o preço do network error com uma key invalidada, e o servidor preserva atomicidade.

Boa-fé do retry após network timeout

key=$(uuidgen)
curl -X POST .../agendamentos -H "Idempotency-Key: $key" -d "$body"
# resposta perdida na rede? retentar com a MESMA key (e mesmo body):
curl -X POST .../agendamentos -H "Idempotency-Key: $key" -d "$body"
# 2ª resposta = 1ª resposta (replay), zero risco de duplicar.
Recomendação: gere um UUID v4 para cada requisição lógica do cliente (cancelar o agendamento 8421 é uma nova requisição, distinta do criar do 8421). Não reutilize keys entre operações diferentes.

↑ Topo

LGPD em POSTs e PATCHs — mascaramento e retenção

Os POSTs de agenda e os POST/PATCH de pacientes carregam PII densa no corpo (cpf, data_nascimento, numero_carteirinha, celular, email, endereco, paciente_cpf, paciente_celular, paciente_email…). O gateway aplica mascaramento estruturado no log de auditoria e respeita retenção curta.

Mascaramento de payload no log de auditoria

Todo body de POST e PATCH gravado no log passa por sanitização recursiva antes de ser persistido. Chaves abaixo viram a string literal "[REDACTED]":

CategoriaChaves cobertas
Credenciaissenha, password, secret, token, usuario, api_key, api_secret, x_signature, authorization
Documentoscpf, cnpj, rg, cns, cpf_responsavel, paciente_cpf, paciente_rg
Contato diretocelular, telefone, email, paciente_celular, paciente_email
PII denso (Fase 3*)numero_carteirinha, numero_cartaonacionalsaude, data_nascimento

paciente_nome NÃO é mascarado — é o contexto operacional usado pelo suporte para reconciliar tickets. LGPD aceita em log com retenção curta (ver abaixo).

Snapshots PII em agendamentos sem paciente_id

Quando o agendamento é criado sem paciente_id, o backend grava snapshots (paciente_cpf, paciente_celular, etc.) no registro. Esses campos jamais aparecem no payload de resposta da API pública v1 — whitelist incondicional defensiva nos endpoints de leitura e escrita. Para acessar PII do paciente, use GET /v1/pacientes/{id} com o escopo pacientes:read separado.

PATCH em paciente com laudo já assinado (D-X10 / Fase 3*)

PATCH /v1/pacientes/{id} em campos como data_nascimento ou cpf de um paciente não é bloqueado quando há laudos PDF já assinados associados. O backend do CMS legado permite a edição (operadora corrige typos de cadastro o tempo todo), e o gateway mantém paridade.

Consequência: laudos PDF já assinados preservam o cabeçalho do momento da assinatura (snapshot binário imutável). O que muda após o PATCH é o cadastro corrente do paciente — laudos futuros sairão com os dados atualizados. O audit trail (atualizado_por_chave_id + entrada em api_requests_log com payload mascarado) preserva quem mudou o quê e quando. Para o cliente externo: revisar cadastro antes de assinar é responsabilidade da clínica.

Retenção

DadoRetençãoNotas
Store de idempotência 24 horas TTL da Idempotency-Key. Cleanup oportunista.
Log de auditoria das requisições 30 dias Mantém PII operacional (incluindo paciente_nome em claro) acessível para troubleshooting de curto prazo, sem virar passivo LGPD de longo prazo.

Direito do titular

  • Cancelar agendamento NÃO apaga o histórico. A entrada permanece no histórico do agendamento com a origem (API) e identificador da chave usada, para rastreabilidade legal.
  • Para anonimização definitiva (exclusão por solicitação do titular), encaminhe ao DPO da clínica — operação manual, fora da API.

↑ Topo

GET /v1/ping — Health check

Retorna metadados do gateway sem chamar o backend Sivoe. Útil para validar credenciais HMAC e medir latência sem overhead de query no banco.

Escopo exigido

Nenhum — aceita qualquer chave válida (mesmo sem escopos atribuídos).

Headers obrigatórios

Os 3 headers comuns (X-API-Key, X-Timestamp, X-Signature).

Parâmetros

Nenhum (path nem query).

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
401AUTH_*Falha de autenticação (header ausente, assinatura inválida, timestamp fora da janela…)

Schema de resposta (200)

CampoTipoDescrição
data.okbooleanSempre true
data.servicestring"Sivoe API Gateway"
data.versionstring"v1"
data.server_timeISO 8601 UTCHora corrente do servidor
data.backend_calledbooleanfalse em GET (não chamou backend)
data.auth.api_key_maskedstringChave parcialmente mascarada (ex.: svp_live_a1b2****c3d4)
data.auth.chave_idintegerID interno da chave
data.auth.clinicastringSlug do tenant resolvido (ex.: sandbox)
data.auth.medico_idinteger0 se a chave não tem auto-filtro; caso contrário, o medico_id vinculado
data.auth.escoposarray<string>Lista de escopos da chave
data.echo.methodstring"GET"
data.echo.bodynullSempre null em GET

Notas e regras especiais

Endpoint não consulta o backend — ideal para warm-up do cache de Authorization antes de uma rajada de chamadas. A 1ª chamada continua sendo cold (~2300ms) porque a autenticação interna roda mesmo sem hit no banco aplicacional.

Exemplo cURL

API_KEY="svp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API_SECRET="<64 chars hex>"
TS=$(date +%s)
METHOD="GET"
PATH_Q="/v1/ping"
BODY=""
PAYLOAD="${TS}.${METHOD} ${PATH_Q}
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": {
    "ok": true,
    "service": "Sivoe API Gateway",
    "version": "v1",
    "server_time": "2026-05-19T20:00:00Z",
    "backend_called": false,
    "auth": {
      "api_key_masked": "svp_live_a1b2****c3d4",
      "chave_id": 15,
      "clinica": "sandbox",
      "medico_id": 0,
      "escopos": ["pacientes:read", "exames:read", "laudos:read"]
    },
    "echo": { "method": "GET", "body": null }
  },
  "meta": { "request_id": "a1b2c3d4e5f6", "duracao_ms": 12 },
  "error": null
}

Try it out → ↑ Topo

POST /v1/ping — Eco com PII redacted

Variante POST do health check: ecoa o body recebido em data.echo.body após sanitização de PII. Útil para depurar a montagem do payload assinado quando a assinatura falha. Body máximo de eco: 2 KB.

Escopo exigido

Nenhum — aceita qualquer chave válida.

Headers obrigatórios

Os 3 headers comuns + Content-Type: application/json.

Body

JSON arbitrário, até 2 KB. Campos sensíveis são redacted antes do eco.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK (body ecoado em data.echo.body)
401AUTH_*Falha de autenticação
413VAL_INVALID_PARAMBody excede 2 KB

Campos redacted (PII)

Campos cujo nome (case-insensitive) bate com a lista abaixo viram a string literal "[REDACTED]" antes do eco — defesa em profundidade para nunca devolver PII em logs de debug:

  • senha, password
  • cpf, cns, rg
  • token, api_secret

Exemplo cURL

BODY='{"teste":"valor","cpf":"12345678900"}'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/ping
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/ping"

Exemplo de resposta (200)

{
  "data": {
    "ok": true,
    "service": "Sivoe API Gateway",
    "version": "v1",
    "server_time": "2026-05-19T20:00:00Z",
    "backend_called": false,
    "auth": { "api_key_masked": "svp_live_a1b2****c3d4", "chave_id": 15,
              "clinica": "sandbox", "medico_id": 0,
              "escopos": ["pacientes:read","exames:read","laudos:read"] },
    "echo": {
      "method": "POST",
      "body":   { "teste": "valor", "cpf": "[REDACTED]" }
    }
  },
  "meta": { "request_id": "...", "duracao_ms": 14 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/config — Capacidades e defaults do tenant

Novo em 1.1.0. Retorna a versão do gateway, o contrato em vigor, os escopos da chave, os enums que o gateway valida (domínios fechados como sexo, arquivo_tipo e status_laudo) e os defaults do tenant para o POST /v1/exames. Use-o para descobrir o que pode ser omitido ao criar um exame: cada default int significa que aquele *_id pode ser deixado de fora; null significa que o tenant não tem default e o cliente precisa mandar o *_id ou um alias correspondente.

Escopo exigido

Nenhum — aceita qualquer chave válida (sem escopo de escrita).

Headers obrigatórios

Os 3 headers comuns (X-API-Key, X-Timestamp, X-Signature).

Parâmetros

Nenhum (path nem query).

Cache

Resposta cacheável: Cache-Control: private, max-age=300 (5 min). Consulte uma vez no início da integração e reuse o resultado durante a sessão.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
401AUTH_*Falha de autenticação (header ausente, assinatura inválida, timestamp fora da janela…)

Schema de resposta (200)

CampoTipoDescrição
data.versionstringVersão do gateway (ex.: "1.1.0").
data.api_versionstring"v1" (contrato em vigor).
data.escoposarray<string>Escopos atribuídos à chave.
data.enums.sexoarray<string>Valores aceitos em sexo (POST/PATCH /v1/pacientes): MAS, FEM, OUT.
data.enums.arquivo_tipoarray<string>Valores do ?tipo= em GET /v1/exames/{id}/arquivo: exame, laudo.
data.enums.status_laudoarray<string>Filtro/saída de status de laudo: PENDENTE, EMITIDO, ASSINADO.
data.enums.prioridadearray<integer>Prioridade no POST /v1/exames: 1 (Normal), 2 (Alta), 3 (Urgente).
data.enums.requer_laudoarray<integer>Flag requer_laudo no POST /v1/exames: 0, 1.
data.defaults.unidade_idinteger | nullDefault da unidade; null = sem default.
data.defaults.medico_exame_idinteger | nullDefault do médico do exame; null = sem default.
data.defaults.medico_laudo_idinteger | nullDefault do médico do laudo; null = sem default.
data.defaults.convenio_idinteger | nullDefault do convênio; null = sem default.

Exemplo cURL

API_KEY="svp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
API_SECRET="<64 chars hex>"
TS=$(date +%s)
METHOD="GET"
PATH_Q="/v1/config"
BODY=""
PAYLOAD="${TS}.${METHOD} ${PATH_Q}
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": {
    "version": "1.1.0",
    "api_version": "v1",
    "escopos": ["exames:read", "exames:write", "catalogos:read"],
    "enums": {
      "sexo": ["MAS", "FEM", "OUT"],
      "arquivo_tipo": ["exame", "laudo"],
      "status_laudo": ["PENDENTE", "EMITIDO", "ASSINADO"],
      "prioridade": [1, 2, 3],
      "requer_laudo": [0, 1]
    },
    "defaults": { "unidade_id": 1, "medico_exame_id": 1093, "medico_laudo_id": 1093, "convenio_id": null }
  },
  "meta": { "version": "1.1.0", "timestamp": "2026-06-19T10:00:00-03:00", "request_id": "1a2b3c4d5e6f7890", "duracao_ms": 38 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/pacientes — Listar pacientes

Listagem paginada de pacientes do tenant da chave. Pacientes não têm relação direta com médico no schema legado, portanto não há auto-filtro.

Escopo exigido

pacientes:read

Parâmetros (query, todos opcionais)

NomeTipoDefaultObrigatórioDescrição
cpf string (11 dígitos) não Match exato; rejeita qualquer valor fora de ^\d{11}$.
nome string não Mínimo 3 chars após trim; busca ILIKE %nome%.
periodo_cadastro_inicio YYYY-MM-DD não Filtro inclusivo. Casa pacientes com log PACIENTE CADASTRADO.
periodo_cadastro_fim YYYY-MM-DD não Filtro inclusivo (até 23:59:59).
ativo 0 ou 1 não Whitelist estrita; qualquer outro valor → 400 VAL_INVALID_PARAM.
limit integer 1–200 50 não Teto absoluto 200 para impedir dump massivo.
offset integer ≥ 0 0 não Inteiro estrito; combine com meta.has_more para paginar.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
400VAL_INVALID_PARAMParam fora da whitelist (error.details.field indica qual)
401AUTH_*Falha de autenticação
403AUTH_INSUFFICIENT_SCOPEChave sem pacientes:read
405METHOD_NOT_ALLOWEDOutro método que não GET

Schema de resposta (200)

CampoTipoDescrição
data[].idintegerPK do paciente
data[].nomestringNome completo em CAIXA ALTA (padrão do legado)
data[].cpfstring (11 dígitos) ou nullCPF sem formatação
data[].data_nascimentoYYYY-MM-DD ou null
data[].sexoenum M / F ou null
data[].telefonestring ou null
data[].emailstring ou null
data[].ativoboolean
data[].criado_emISO 8601 UTC
meta.limitintegerEco do limit aplicado
meta.offsetintegerEco do offset
meta.totalintegerTotal que satisfaz o filtro no tenant
meta.has_morebooleantrue se offset+limit < total

Campos excluídos por LGPD/segurança

  • senha — credencial; jamais sai do banco.
  • usuario — usuário de acesso ao portal (write-only; enviado no POST/PATCH, nunca retornado).
  • data_alteracao_senha — telemetria interna.
  • foto_usuario — payload pesado, fora do escopo da API.
  • observacao — campo livre, pode conter PII de terceiros.
  • nome_responsavel, cpf_responsavel — PII de terceiros (acompanhante).
  • matricula — chave interna de convênio/clínica.
  • Flags de perfil internos (admin, gerente) — controle de acesso interno da clínica.

Notas e regras especiais

O total reflete o tenant da chave; chaves de clínicas diferentes nunca veem o mesmo paciente (isolamento de tenant). O endpoint não aplica auto-filtro de médico (pacientes não têm medico_id direto no legado).

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/pacientes?nome=joao&limit=10"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -s \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": [{
    "id": 1133,
    "nome": "JOAO TESTE",
    "cpf": "12345678900",
    "data_nascimento": "1990-05-17",
    "sexo": "M",
    "telefone": "11999999999",
    "email": "joao@exemplo.com",
    "ativo": true,
    "criado_em": "2024-01-15T10:30:00Z"
  }],
  "meta": {
    "limit": 10, "offset": 0,
    "total": 1247, "has_more": true,
    "request_id": "a1b2c3d4e5f6", "duracao_ms": 35
  },
  "error": null
}

Try it out → ↑ Topo

GET /v1/pacientes/{id} — Detalhe do paciente

Retorna dados completos do paciente, incluindo endereço, estado civil e (se cadastrado) convênio.

Escopo exigido

pacientes:read

Parâmetros (path)

NomeTipoObrigatórioDescrição
id integer > 0 sim PK do paciente (usuarios.id no legado).

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
400VAL_INVALID_PARAMid não é inteiro positivo
401AUTH_*Falha de autenticação
403AUTH_INSUFFICIENT_SCOPEChave sem pacientes:read
404RES_NOT_FOUNDID inexistente no tenant (mensagem genérica, sem details)

Schema de resposta (200)

CampoTipoDescrição
data.idintegerPK
data.saudacaostringEx.: "SR.", "SRA.", "DR."
data.nomestringNome em caixa alta
data.nome_registrostringNome social/registro (pode ser vazio)
data.numero_prontuariostringIdentificador interno da clínica
data.cpfstring ou null11 dígitos
data.rgstring ou null
data.data_nascimentoYYYY-MM-DD ou null
data.sexoM/F ou null
data.estado_civilobjeto {id, nome} ou nullFK estado_civil
data.telefonestring ou null
data.celularstring ou null
data.emailstring ou null
data.enderecoobjeto Enderecocep, logradouro, numero, complemento, bairro, cidade, estado
data.convenioobjeto {id, nome, numero_carteirinha} ou nullPode vir null se paciente sem convênio
data.ativoboolean
data.criado_emISO 8601 UTC
data.atualizado_emISO 8601 UTC ou null

Campos excluídos por LGPD/segurança

Mesma lista de listar pacientes — exclui credenciais, foto, observações livres, PII de responsável e flags de perfil interno.

Notas e regras especiais

estado_civil e convenio são sub-objetos que podem vir null quando o paciente não tem o vínculo cadastrado. 404 RES_NOT_FOUND usa mensagem genérica (sem details) para não vazar a existência de IDs.

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/pacientes/1133"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -s \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": {
    "id": 1133,
    "saudacao": "SR.",
    "nome": "JOAO TESTE",
    "nome_registro": "",
    "numero_prontuario": "P000123",
    "cpf": "12345678900",
    "rg": "MG-12.345.678",
    "data_nascimento": "1990-05-17",
    "sexo": "M",
    "estado_civil": { "id": 1, "nome": "SOLTEIRO" },
    "telefone": "11999999999",
    "celular": "11988888888",
    "email": "joao@exemplo.com",
    "endereco": {
      "cep": "01234567",
      "logradouro": "RUA DAS FLORES",
      "numero": "100",
      "complemento": "APTO 5",
      "bairro": "CENTRO",
      "cidade": "SAO PAULO",
      "estado": "SP"
    },
    "convenio": { "id": 7, "nome": "UNIMED", "numero_carteirinha": "1234567890" },
    "ativo": true,
    "criado_em": "2024-01-15T10:30:00Z",
    "atualizado_em": "2026-05-01T14:22:00Z"
  },
  "meta": { "request_id": "...", "duracao_ms": 12 },
  "error": null
}

Try it out → ↑ Topo

POST /v1/pacientes — Criar paciente (Fase 3*)

Idempotency-Key obrigatório. POST e PATCH de paciente seguem o mesmo contrato da agenda. UUID v4 recomendado, TTL 24h. Sem ele → 400 VAL_INVALID_PARAM. Ver Idempotência.

Cria um paciente novo no tenant da chave. O paciente é gravado como perfil PACIENTE (string '1') por defesa em profundidade do gateway — não é possível criar médico, admin ou outros perfis via este endpoint. numero_prontuario é opcional; quando ausente, o backend auto-gera e a resposta inclui o valor real gravado.

Escopo exigido

pacientes:write

Headers obrigatórios

Os 3 headers comuns + Content-Type: application/json + Idempotency-Key.

Body (JSON)

CampoTipoObrigatórioDescrição
nomestring (5–100 chars)simNome completo. Backend grava em caixa alta (paridade com CMS).
data_nascimentoYYYY-MM-DDsimEntre 130 anos atrás e hoje (inclusive).
cpfstring (11 dígitos)condicionalObrigatório se cpf_obrigatorio=1 na getReferencias do tenant. Quando enviado: regex + algoritmo de dígito verificador (validação dupla: gateway + backend).
celularstringcondicionalObrigatório se celular_obrigatorio=1 na getReferencias.
saudacaostringnãoEx.: "Sr.", "Sra.", "Dr.".
nome_registrostringnãoNome social/registro civil.
sexoMAS | FEM | OUTnãoString. O backend legado aceita 1/2/3 no CMS, mas a API pública exige a string.
rgstringnãoRG sem máscara obrigatória.
estado_civil_idintegernãoFK estado_civil.id.
telefonestringnãoTelefone fixo (sem máscara).
emailstringnãoE-mail válido.
numero_prontuariostringnãoQuando ausente, o backend auto-gera (resposta inclui o valor real). Quando enviado e já em uso por outro paciente → 422 MEDICAL_RECORD_ALREADY_EXISTS.
enderecoobjeto EndereconãoSub-objeto {cep, logradouro, numero, complemento, bairro, cidade_id, estado}. Presente substitui inteiro; ausente mantém vazio.
convenioobjetonãoSub-objeto {id, plano_id, numero_carteirinha}. Presente substitui inteiro.
ativobooleannãoDefault backend (geralmente true no perfil PACIENTE).
usuariostring (1–50 chars)nãoUsuário de acesso ao portal — útil quando gerado em sistema externo (ex.: Tasy) e entregue em cartão impresso. Aceita A–Z a–z 0–9 . _ - @. Verificado quanto a duplicidade (case-insensitive) → 422 USUARIO_ALREADY_EXISTS (usuário repetido travaria o acesso de ambos os pacientes). Ausente → backend gera usuário aleatório e o paciente entra pelo CPF.
senhastring (1–50 chars)nãoWrite-only (release 1.3.0). Senha inicial de acesso ao portal do paciente — pensada para clínicas que geram a senha em sistema externo (ex.: Tasy) e a entregam ao paciente em cartão impresso. Sem validação de formato (apenas não vazia, máx. 50). Nunca retornada nas respostas; mascarada nos logs de auditoria. Ausente → backend gera senha aleatória.
senha_temporariabooleannãoSó tem efeito com senha presente. Default true: a senha é tratada como temporária (orienta troca no 1º acesso). Envie false para gravar como definitiva. Regras de robustez do portal ainda podem exigir troca se a senha for fraca.

Campos bloqueados (D-X14 / Fase 3*)

As chaves abaixo são descartadas silenciosamente antes de chegar ao backend — defesa em profundidade para evitar que cliente externo crie médico/admin ou sobrescreva dados internos. (Obs.: usuario, senha e senha_temporaria não são bloqueados — são credenciais do portal aceitas no POST e no PATCH; vide tabela do body acima.)

data_alteracao_senha, foto_usuario, imagem, observacao, visualizador_exames_modo, paciente (forçado para '1'), perfil, admin, gerente, exames_atendimento_perfil_id, restricoes, nome_responsavel, cpf_responsavel, matricula.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
201Created. Headers: Location: /v1/pacientes/{id}, Idempotency-Replay: false.
400VAL_INVALID_PARAMIdempotency-Key ausente/inválida, campos obrigatórios faltando, formato inválido (incl. CPF com dígito verificador errado), sexo fora de MAS|FEM|OUT.
403AUTH_INSUFFICIENT_SCOPEChave sem pacientes:write.
409CONCURRENT_REQUESTMesma Idempotency-Key em flight. Retry-After: 2.
422IDEMPOTENCY_CONFLICTMesma key + body diferente.
422CPF_ALREADY_EXISTSCPF já cadastrado em outro paciente. details.paciente_id_existente + details.nome guiam o cliente a fazer GET/PATCH do registro existente em vez de criar um novo.
422MEDICAL_RECORD_ALREADY_EXISTSnumero_prontuario já em uso. details.paciente_id_existente + details.nome.
422USUARIO_ALREADY_EXISTSusuario já em uso por outro paciente (case-insensitive). details.nome + details.paciente_id_existente quando disponível. Evita travar a autenticação de ambos os pacientes.
422BUSINESS_RULEValidação do backend (ex.: campo obrigatório faltando por getReferencias do tenant).
503SERVICE_UNAVAILABLENetwork error ao chamar backend. Cacheado por 24h — retry com mesma key devolve 503 (ver Idempotência).

Schema de resposta (201)

Mesma whitelist de GET detalhe (PacienteDetalhe) acrescida de 4 colunas de auditoria expostas a partir da Fase 3*:

CampoTipoDescrição
data.criado_emISO 8601 UTCTimestamp do INSERT (migração 85 adicionou a coluna).
data.atualizado_emISO 8601 UTC ou nullnull em paciente recém-criado.
data.criado_por_chave_idinteger ou nullID da chave HMAC que criou o registro. null quando criado via CMS legado (paridade bit-idêntica preservada).
data.atualizado_por_chave_idinteger ou nullnull em paciente recém-criado.

Exemplo cURL

BODY='{
  "saudacao": "Sr.",
  "nome": "JOAO DA SILVA",
  "cpf": "12345678901",
  "data_nascimento": "1990-05-15",
  "sexo": "MAS",
  "celular": "31988887777",
  "email": "joao@example.com",
  "endereco": {
    "cep": "30000000",
    "logradouro": "Rua das Flores",
    "numero": "123",
    "bairro": "Centro",
    "cidade_id": 3106200,
    "estado": "MG"
  },
  "convenio": { "id": 12, "plano_id": 34, "numero_carteirinha": "987654321" },
  "usuario": "joao.silva",
  "senha": "Tasy7K2m",
  "senha_temporaria": false
}'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/pacientes
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/pacientes"

Exemplo de resposta (201)

HTTP/1.1 201 Created
Location: /v1/pacientes/4823
Idempotency-Key: 7c6e2cad-2026-05-20-001
Idempotency-Replay: false
X-Api-Version: v1
X-Request-Id: 8b4f1c3a2e6d9b78
Content-Type: application/json; charset=utf-8
{
  "data": {
    "id": 4823,
    "saudacao": "Sr.",
    "nome": "JOAO DA SILVA",
    "nome_registro": "",
    "numero_prontuario": "P-2026-4823",
    "cpf": "12345678901",
    "rg": "",
    "data_nascimento": "1990-05-15",
    "sexo": "MAS",
    "estado_civil": null,
    "telefone": "",
    "celular": "31988887777",
    "email": "joao@example.com",
    "endereco": {
      "cep": "30000000",
      "logradouro": "Rua das Flores",
      "numero": "123",
      "complemento": "",
      "bairro": "Centro",
      "cidade": "Belo Horizonte",
      "estado": "MG"
    },
    "convenio": { "id": 12, "plano_id": 34, "numero_carteirinha": "987654321" },
    "ativo": true,
    "criado_em": "2026-05-20T14:23:11Z",
    "atualizado_em": null,
    "criado_por_chave_id": 17,
    "atualizado_por_chave_id": null
  },
  "meta": {
    "request_id": "8b4f1c3a2e6d9b78",
    "duracao_ms": 712,
    "idempotent_replay": false
  },
  "error": null
}

Notas e regras especiais

  • CPF com algoritmo: 11 dígitos + dígitos verificadores válidos. Validação dupla (gateway + backend) por defesa em profundidade. Quando faltar a clínica ter cpf_obrigatorio=1, o CPF é opcional — mas se enviado, o algoritmo é checado igualmente.
  • Race CPF entre keys distintas: Idempotency-Key protege a mesma chave. Duas keys diferentes em <50ms com o mesmo CPF podem ambas passar (risco residual aceito, ver ADR-005 D-X9 / D-X12). Cliente que precise de garantia absoluta deve fazer GET /v1/pacientes?cpf=… antes do POST.
  • Snapshot vs. paciente: esses 4 campos de auditoria (criado_em, atualizado_em, criado_por_chave_id, atualizado_por_chave_id) também passaram a aparecer no GET detalhe a partir da Fase 3*.

Try it out → ↑ Topo

PATCH /v1/pacientes/{id} — Atualizar paciente parcialmente (Fase 3*)

Idempotency-Key obrigatório. Mesma semântica do POST. Ver Idempotência.

Atualização parcial do paciente conforme RFC 7396 JSON Merge Patch: campo omitido não é tocado; campo com valor null ou "" é resetado; sub-objeto endereco / convenio presente substitui inteiro o atual (sem sub-merge na v1); sub-objeto {} ou null zera o sub-objeto. O id é sempre preservado (semântica HTTP do PATCH).

Escopo exigido

pacientes:write

Headers obrigatórios

3 headers comuns + Content-Type: application/json + Idempotency-Key.

Path param

NomeTipoDescrição
idinteiro > 0PK do paciente. Inteiro estrito; 0 ou negativo → 400 VAL_INVALID_PARAM.

Body (JSON parcial)

Qualquer subconjunto dos campos aceitos pelo POST (exceto os campos bloqueados). Mesmas regras de validação por campo — CPF com algoritmo, sexo na whitelist MAS|FEM|OUT, datas válidas. A partir da release 1.4.0, os campos usuario, senha e senha_temporaria também são aceitos no PATCH — permitem redefinir o acesso ao portal (ex.: paciente que esqueceu usuário/senha). Omiti-los preserva as credenciais atuais; enviar usuario/senha as sobrescreve. senha continua write-only e ambos seguem mascarados nos logs.

Sub-objetos: endereco e convenio

  • "endereco": { "cep": "31000000", "numero": "200" }substitui inteiro o endereço atual. Campos não enviados ficam vazios no resultado.
  • "endereco": {} ou "endereco": nullzera o endereço.
  • Omitir "endereco"mantém o endereço atual intacto.

Mesma regra para convenio. Sub-merge dentro do sub-objeto não existe na v1 — quem quiser preservar campos do sub-objeto deve enviar todos eles ou usar GET + PATCH na sequência.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK. id preservado. atualizado_em e atualizado_por_chave_id recebem valores; criado_em e criado_por_chave_id são preservados.
400VAL_INVALID_PARAM{id} ≤ 0, Idempotency-Key ausente/inválida, ou campo individual inválido após merge (CPF com dígito errado, sexo fora de MAS|FEM|OUT, etc.).
403AUTH_INSUFFICIENT_SCOPEChave sem pacientes:write.
404RES_NOT_FOUNDPaciente {id} não existe no tenant.
409CONCURRENT_REQUESTMesma key em flight.
422IDEMPOTENCY_CONFLICTMesma key + body diferente.
422CPF_ALREADY_EXISTSPATCH alterando CPF para CPF de outro paciente. PATCH com o mesmo CPF do próprio paciente → 200 OK idempotente.
422MEDICAL_RECORD_ALREADY_EXISTSPATCH alterando numero_prontuario para um já em uso.
422USUARIO_ALREADY_EXISTSPATCH redefinindo usuario para um já em uso por outro paciente (case-insensitive). Redefinir para o mesmo usuário do próprio paciente é idempotente.
422BUSINESS_RULEValidação do backend após merge.
503SERVICE_UNAVAILABLENetwork error. Cacheado por 24h — mesma key devolve 503 replay.

Exemplo cURL — atualizar só e-mail + número de prontuário

BODY='{
  "email": "joao.novo@example.com",
  "numero_prontuario": "P-2026-0001-A"
}'
TS=$(date +%s)
PAYLOAD="${TS}.PATCH /v1/pacientes/4823
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X PATCH \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/pacientes/4823"

Exemplo de resposta (200 — id preservado)

{
  "data": {
    "id": 4823,
    "saudacao": "Sr.",
    "nome": "JOAO DA SILVA",
    "nome_registro": "",
    "numero_prontuario": "P-2026-0001-A",
    "cpf": "12345678901",
    "rg": "",
    "data_nascimento": "1990-05-15",
    "sexo": "MAS",
    "estado_civil": null,
    "telefone": "",
    "celular": "31988887777",
    "email": "joao.novo@example.com",
    "endereco": {
      "cep": "30000000",
      "logradouro": "Rua das Flores",
      "numero": "123",
      "complemento": "",
      "bairro": "Centro",
      "cidade": "Belo Horizonte",
      "estado": "MG"
    },
    "convenio": { "id": 12, "plano_id": 34, "numero_carteirinha": "987654321" },
    "ativo": true,
    "criado_em": "2026-05-20T14:23:11Z",
    "atualizado_em": "2026-05-20T14:51:33Z",
    "criado_por_chave_id": 17,
    "atualizado_por_chave_id": 17
  },
  "meta": { "request_id": "5f8d3a1b2c4e7d96", "duracao_ms": 503, "idempotent_replay": false },
  "error": null
}

PATCH em paciente com laudo PDF assinado (D-X10)

PATCH em data_nascimento, cpf ou qualquer outro campo do paciente não é bloqueado quando há laudos PDF já assinados associados. Os PDFs assinados são snapshots binários imutáveis — preservam o cadastro do momento da assinatura; apenas a janela atual do paciente muda. Ver LGPD em POSTs e PATCHs para detalhes operacionais.

Notas e regras especiais

  • Idempotência em PATCH: mesmo key + mesmo body devolve 200 replay (Idempotency-Replay: true). Body diferente → 422 IDEMPOTENCY_CONFLICT. Network error é cacheado como 503 — retry da mesma key devolve 503 replay; o cliente deve verificar via GET /v1/pacientes/{id} ou gerar nova key.
  • PATCH idempotente do próprio CPF: enviar "cpf": "<cpf_atual>" no PATCH não gera 422 CPF_ALREADY_EXISTS — o backend reconhece que é o mesmo paciente. Só dispara o erro quando o CPF pertence a outro registro.
  • Sub-objeto vazio: "endereco": {} apaga o endereço cadastrado — alguns campos retornam string vazia (paridade com CMS legado: cep e numero normalizam "0""" na resposta).

Try it out → ↑ Topo

POST /v1/pacientes/unificar — Unificar pacientes duplicados

⚠ Operação atômica e IRREVERSÍVEL. Migra todos os dados (cadastro, exames, laudos, prontuários, agendamentos, etc.) do paciente duplicado para o principal e exclui o duplicado, dentro de uma única transação no backend. Não há desfazer. Use só após confirmar que os dois IDs são a mesma pessoa.
Idempotency-Key obrigatório. Mesmo contrato dos demais POST/PATCH de paciente. UUID v4 recomendado, TTL 24h. Sem ele → 400 VAL_INVALID_PARAM. Ver Idempotência.

Resolve duplicidade de cadastro: tudo que apontava para o paciente duplicado passa a apontar para o principal, e o registro duplicado é removido fisicamente. O gateway apenas orquestra — a transação roda no backend (pacientes/unificar). A resposta é enxuta: confirma a unificação sem expor quais tabelas foram migradas nem a contagem de linhas.

Escopo exigido

pacientes:write (o mesmo de criar/atualizar — não há escopo novo).

Headers obrigatórios

Os 3 headers comuns + Content-Type: application/json + Idempotency-Key.

Body (JSON)

CampoTipoObrigatórioDescrição
paciente_id_principalinteiro > 0simID do paciente que SERÁ MANTIDO (destino). Recebe todas as referências do duplicado.
paciente_id_duplicadointeiro > 0simID do paciente que SERÁ EXCLUÍDO (origem dos dados). Deve ser diferente de paciente_id_principal.

Cada ID aceita inteiro JSON (4823) ou string só de dígitos ("4823"). Ausente, vazio, não-numérico ou ≤ 0400 VAL_INVALID_PARAM.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200Unificação concluída. Body { paciente_id_principal, paciente_id_duplicado, status: "unificado" }. Header Idempotency-Replay: false.
400VAL_INVALID_PARAMIdempotency-Key ausente/inválida, body não-JSON, ou ID ausente/não-inteiro/≤ 0.
403AUTH_INSUFFICIENT_SCOPEChave sem pacientes:write.
409CONCURRENT_REQUESTMesma Idempotency-Key em flight. Retry-After: 2.
422PACIENTES_UNIFICACAO_MESMO_IDpaciente_id_principal igual a paciente_id_duplicado — não faz sentido unificar um paciente consigo mesmo.
422IDEMPOTENCY_CONFLICTMesma key + body diferente.
422BUSINESS_RULERegra de negócio do backend: paciente não encontrado ou inválido. error.message traz a causa.
503SERVICE_UNAVAILABLEBackend indisponível. Cacheado por 24h — retry com mesma key devolve 503 (ver Idempotência).
504SERVICE_TIMEOUTTimeout do backend ao processar a unificação.

Exemplo cURL

BODY='{
  "paciente_id_principal": 4823,
  "paciente_id_duplicado": 5190
}'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/pacientes/unificar
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/pacientes/unificar"

Exemplo de resposta (200)

HTTP/1.1 200 OK
Idempotency-Key: 7c6e2cad-2026-06-20-001
Idempotency-Replay: false
X-Api-Version: v1
X-Request-Id: 9c5e2dba3f7e0c89
Content-Type: application/json; charset=utf-8
{
  "data": {
    "paciente_id_principal": 4823,
    "paciente_id_duplicado": 5190,
    "status": "unificado"
  },
  "meta": {
    "request_id": "9c5e2dba3f7e0c89",
    "duracao_ms": 318,
    "idempotent_replay": false
  },
  "error": null
}

Exemplo de erro (422 — IDs iguais)

{
  "data": null,
  "meta": { "request_id": "1a2b3c4d5e6f7899", "duracao_ms": 4 },
  "error": {
    "code": "PACIENTES_UNIFICACAO_MESMO_ID",
    "message": "Os pacientes principal e duplicado devem ser diferentes.",
    "details": { "field": "paciente_id_duplicado" }
  }
}

Exemplo de erro (422 — regra de negócio do backend)

{
  "data": null,
  "meta": { "request_id": "1a2b3c4d5e6f789a", "duracao_ms": 142 },
  "error": {
    "code": "BUSINESS_RULE",
    "message": "Paciente nao encontrado.",
    "details": null
  }
}

Exemplo de erro (403 — escopo insuficiente)

{
  "data": null,
  "meta": { "request_id": "1a2b3c4d5e6f789b", "duracao_ms": 2 },
  "error": {
    "code": "AUTH_INSUFFICIENT_SCOPE",
    "message": "Chave sem escopo pacientes:write.",
    "details": null
  }
}

Notas e regras especiais

  • Sem escopo novo: reusa pacientes:write — a mesma chave que cria/atualiza paciente pode unificar. Não é preciso reprovisionar a chave.
  • Irreversibilidade: a exclusão do duplicado é física. Não existe endpoint de “desunificar”. Confirme os IDs com GET /v1/pacientes/{id} antes de chamar.
  • Idempotência protege replay, não troca de IDs: reenviar a mesma key com o mesmo body devolve o 200 cacheado (mesmo após o duplicado já ter sido excluído). Trocar qualquer ID exige nova key.
  • Privacidade: a resposta não revela contagem de registros migrados nem nomes de tabelas — apenas confirma status: "unificado".

Try it out → ↑ Topo

GET /v1/exames — Listar exames

Listagem paginada de exames do tenant da chave, com auto-filtro SIMPLES de médico e status_laudo derivado em SQL.

Escopo exigido

exames:read

Parâmetros (query, todos opcionais)

NomeTipoDefaultObrigatórioDescrição
paciente_id integer > 0 não Filtra exames de um paciente específico.
medico_id integer > 0 não Ignorado se a chave tem medico_id (auto-filtro vence — ver §8).
tipo_exame_id integer > 0 não FK exame.id (dicionário compartilhado entre tenants).
periodo_realizado_inicio YYYY-MM-DD não Inclusivo (exames.data::DATE >= valor).
periodo_realizado_fim YYYY-MM-DD não Inclusivo.
status_laudo enum não Whitelist: PENDENTE, EMITIDO, ASSINADO (ver mapping em Notas).
limit integer 1–200 50 não Teto absoluto 200.
offset integer ≥ 0 0 não Inteiro estrito.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
400VAL_INVALID_PARAMParam fora da whitelist (error.details.field indica qual)
401AUTH_*Falha de autenticação
403AUTH_INSUFFICIENT_SCOPEChave sem exames:read
405METHOD_NOT_ALLOWEDOutro método que não GET

Schema de resposta (200)

CampoTipoDescrição
data[].idintegerPK do exame (exames.exameid)
data[].pacienteobjeto {id, nome}Sub-objeto compacto (PacienteRef)
data[].medico_exameobjeto {id, nome} ou nullMédico responsável pela realização do exame
data[].medico_laudoobjeto {id, nome} ou nullMédico responsável pelo laudo
data[].solicitanteobjeto {id, nome} ou nullMédico solicitante (pode ser externo)
data[].tipo_exameobjeto {id, nome}Ex.: {id:7, nome:"CAMPO VISUAL"}
data[].data_realizadoYYYY-MM-DDData clínica do exame
data[].status_laudoenum PENDENTE/EMITIDO/ASSINADODerivado em SQL (ver Notas)
data[].tem_arquivo_examebooleantrue se PDF do exame anexado
data[].tem_arquivo_laudobooleantrue se PDF do laudo anexado
data[].criado_emISO 8601 UTC
meta.limit/offset/total/has_moreinteger/booleanPaginação padrão

Campos excluídos por LGPD/segurança

  • arquivoexame, arquivolaudo — paths S3 brutos; para baixar o PDF, use GET /v1/exames/{id}/arquivo.
  • cid — código clínico sensível, não exposto na listagem.
  • Anotações internas, observações de auditoria — campo livre, pode conter PII.
  • ip, dispositivo, user-agent de upload — telemetria operacional.
  • Campos financeiros (valor, convênio do exame) — fora do escopo desta API.

Notas e regras especiais

Auto-filtro SIMPLES (§8)

Quando a chave tem medico_id = m, o gateway injeta no payload do backend o predicado medicoexame = m OR medicolaudo = m OR medicosolicitante = m. O parâmetro medico_id da query é ignorado silenciosamente, evitando enumeração de outros médicos.

Mapping de status_laudo (§7)

O campo é derivado em SQL a partir de exames.arquivolaudo + laudo.assinado_digitalmente:

StatusCondiçãoEstado clínico
PENDENTEarquivolaudo vazio/nuloLaudo ainda não emitido
EMITIDOarquivolaudo preenchido e sem laudo.assinado_digitalmente = 1 para o exameLaudo gerado mas não assinado
ASSINADOarquivolaudo preenchido e existe laudo com assinado_digitalmente = 1Laudo final com assinatura ICP-BRASIL
Regra de domínio: laudo com ASSINADO é imutável — endpoints de escrita (quando liberados) recusarão qualquer modificação no exame/laudo após a assinatura ICP.

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/exames?status_laudo=EMITIDO&limit=10"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -s \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": [{
    "id": 99,
    "paciente":     { "id": 1133, "nome": "JOAO TESTE" },
    "medico_exame": { "id": 1125, "nome": "Dr. Carlos" },
    "medico_laudo": { "id": 1093, "nome": "Dra. Beatriz" },
    "solicitante":  null,
    "tipo_exame":   { "id": 7, "nome": "CAMPO VISUAL" },
    "data_realizado":  "2026-03-24",
    "status_laudo":    "EMITIDO",
    "tem_arquivo_exame": true,
    "tem_arquivo_laudo": true,
    "criado_em": "2026-03-24T03:00:00Z"
  }],
  "meta": {
    "limit": 10, "offset": 0,
    "total": 100, "has_more": true,
    "request_id": "...", "duracao_ms": 84
  },
  "error": null
}

Try it out → ↑ Topo

GET /v1/exames/{id} — Detalhe do exame

Retorna dados completos do exame, incluindo URLs relativas para os PDFs (quando anexados). Auto-filtro SIMPLES aplicado: exame fora do filtro retorna 404 RES_NOT_FOUND.

Escopo exigido

exames:read

Parâmetros (path)

NomeTipoObrigatórioDescrição
id integer > 0 sim PK do exame (exames.exameid).

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
400VAL_INVALID_PARAMid não é inteiro positivo
401AUTH_*Falha de autenticação
403AUTH_INSUFFICIENT_SCOPEChave sem exames:read
404RES_NOT_FOUNDID inexistente ou oculto pelo auto-filtro de médico

Schema de resposta (200)

CampoTipoDescrição
data.idintegerPK
data.pacienteobjeto {id, nome}
data.medico_exameobjeto {id, nome} ou null
data.medico_laudoobjeto {id, nome} ou null
data.solicitanteobjeto {id, nome} ou null
data.tipo_exameobjeto {id, nome}
data.data_realizadoYYYY-MM-DD
data.hora_realizadoHH:MM:SS ou null
data.status_laudoenum (ver listar exames)Mesmo derivado SQL
data.laudo_assinado_emISO 8601 UTC ou nullTimestamp da assinatura ICP, quando aplicável
data.tem_arquivo_exameboolean
data.tem_arquivo_laudoboolean
data.url_arquivo_examestring (URL relativa) ou nullEx.: /v1/exames/99/arquivo?tipo=exame
data.url_arquivo_laudostring (URL relativa) ou nullEx.: /v1/exames/99/arquivo?tipo=laudo
data.criado_emISO 8601 UTC
data.atualizado_emISO 8601 UTC ou null

Campos excluídos por LGPD/segurança

Mesma lista de listar exames; URLs url_arquivo_* são relativas (jamais path S3 cru).

Notas e regras especiais

  • Auto-filtro SIMPLES aplicado — exame de outro médico vira 404 RES_NOT_FOUND (mesma mensagem que ID inexistente).
  • Laudo com status_laudo = ASSINADO é imutável (regra de domínio).
  • tipo_exame.id é FK do dicionário compartilhado entre tenants (tabela exame, sem clinica).

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/exames/99"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -s \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": {
    "id": 99,
    "paciente":     { "id": 1133, "nome": "JOAO TESTE" },
    "medico_exame": { "id": 1125, "nome": "Dr. Carlos" },
    "medico_laudo": { "id": 1093, "nome": "Dra. Beatriz" },
    "solicitante":  null,
    "tipo_exame":   { "id": 7, "nome": "CAMPO VISUAL" },
    "data_realizado":   "2026-03-24",
    "hora_realizado":   "23:07:27",
    "status_laudo":     "EMITIDO",
    "laudo_assinado_em": null,
    "tem_arquivo_exame": true,
    "tem_arquivo_laudo": true,
    "url_arquivo_exame": "/v1/exames/99/arquivo?tipo=exame",
    "url_arquivo_laudo": "/v1/exames/99/arquivo?tipo=laudo",
    "criado_em":   "2026-03-24T03:00:00Z",
    "atualizado_em": null
  },
  "meta": { "request_id": "...", "duracao_ms": 87 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/exames/{id}/arquivo — PDF do exame ou laudo (302 S3)

Retorna 302 Found com header Location: apontando para uma URL pré-assinada do S3 válida por 300 segundos. A API não streama bytes — o cliente segue o redirect direto para o S3.

Escopo exigido

exames:read

Parâmetros

LocalNomeTipoObrigatórioDescrição
path id integer > 0 sim PK do exame.
query tipo enum exame / laudo sim Indica qual PDF baixar (exame original ou laudo).

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
302Redirect para presigned S3 (válida 300s)
400VAL_INVALID_PARAMtipo ausente ou fora da whitelist
401AUTH_*Falha de autenticação
403AUTH_INSUFFICIENT_SCOPEChave sem exames:read
404RES_NOT_FOUNDExame inexistente ou oculto pelo auto-filtro
404RES_ARQUIVO_NAO_DISPONIVELRecurso existe mas o PDF S3 ainda não foi anexado
500RES_ARQUIVO_S3_FALHAFalha temporária ao gerar presigned (retry com backoff)

Schema de resposta (302)

CampoTipoDescrição
Header Locationstring (URL S3)Presigned com X-Amz-Expires=300
Bodyvazio

Campos excluídos por LGPD/segurança

O path S3 cru nunca aparece em logs ou no payload da API. Auditoria interna registra apenas {"tipo":"...", "exame_id":<id>, "expires_in":300}.

Notas e regras especiais

  • A presigned expira em 300s. Não cacheie a URL — em retry, refaça o GET /v1/exames/{id}/arquivo.
  • cURL: use -L para seguir o redirect. PHP cURL: mantenha CURLOPT_FOLLOWLOCATION = false e não reassine a URL S3 com seu HMAC (a presigned já autentica contra o S3). Ver FAQ Q12.
  • RES_ARQUIVO_NAO_DISPONIVEL é estado clínico (PDF ainda não gerado), não erro técnico — não faça retry.

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/exames/99/arquivo?tipo=exame"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

# -L segue o 302 automaticamente; -o salva no disco
curl -L -o exame-99.pdf \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (302)

HTTP/1.1 302 Found
Location: https://sivoe4.s3.sa-east-1.amazonaws.com/exames/relatorio-99.pdf
         ?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=300&X-Amz-Signature=...
X-Api-Version: v1
X-Content-Type-Options: nosniff
Cache-Control: no-store

Try it out → ↑ Topo

POST /v1/exames — Criar exame

Idempotency-Key obrigatório. Envie o header Idempotency-Key (UUID v4 recomendado, TTL 24h). Sem ele → 400 VAL_INVALID_PARAM. Mesma key + mesmo body → resposta cacheada (201 replay); mesma key + body diferente → 422 IDEMPOTENCY_CONFLICT. Ver Idempotência.

Cria um exame com metadados e, opcionalmente, arquivos (PDF/imagem do exame e/ou PDF do laudo já assinado) enviados inline em base64. Os IDs de referência vêm dos catálogos read-only (/v1/procedimentos, /v1/medicos, /v1/convenios, /v1/cids, /v1/unidades).

Novo em 1.1.0. Além dos *_id, o body aceita 8 identificadores de negócio (aliases) que o gateway resolve para o *_id correspondente, e o tenant pode definir defaults para unidade_id, medico_exame_id, medico_laudo_id e convenio_id — permitindo criar um exame com um body mínimo. Ambos são aditivos: payloads legados que enviam os *_id continuam válidos sem qualquer mudança. Ver Resolução por identificadores de negócio.

Escopo exigido

exames:write

Headers obrigatórios

Os 3 headers comuns (HMAC) + Content-Type: application/json + Idempotency-Key.

Body (JSON)

CampoTipoObrigatórioDescrição
paciente_idintegersimFK do paciente cadastrado. Pode ser omitido enviando o alias paciente_cpf (ver resolução).
unidade_idintegercondicionalFK da unidade/clínica. Omitível se o tenant tiver default ou se enviar o alias unidade_nome.
procedimento_idintegersimFK do procedimento (tipo de exame). Pode ser omitido enviando o alias procedimento_codigo_tuss ou procedimento_abreviacao.
medico_exame_idintegercondicionalMédico responsável pelo exame. Omitível se o tenant tiver default ou se enviar o alias medico_exame_crm.
medico_laudo_idintegercondicionalMédico laudante. Exigido pelo backend conforme a configuração da clínica (laudo por produção aceita grupos_laudos_id no lugar). Omitível se o tenant tiver default ou via alias medico_laudo_crm.
grupos_laudos_idintegercondicionalGrupo de médicos laudantes (laudo por produção). Descubra em GET /v1/grupos-laudos.
medico_solicitante_idintegercondicionalExigido quando a clínica marca solicitante como obrigatório.
data_exameYYYY-MM-DDnãoDefault: data atual.
numerostringnãoNúmero interno do exame.
referenciastringnãoReferência livre.
observacoesstringnãoAnamnese / observações.
requer_laudo0 | 1não1 = exame requer laudo.
cid_idintegernãoFK do CID.
complementos_idintegernãoFK de complemento. Descubra em GET /v1/complementos.
plano_idintegernãoPlano do convênio.
convenio_idintegernãoConvênio. Omitível se o tenant tiver default ou via alias convenio_codigo_ans / convenio_nome.
prioridade1 | 2 | 3não1 Normal, 2 Alta, 3 Urgente. Default 1.
arquivos.exames[]array {nome, base64}nãoArquivo(s) do exame. Cap 20 MB/arquivo. PDF/PNG/JPG/JPEG.
arquivos.laudos[]array {nome, base64}nãoPDF(s) do laudo já assinado, quando exame e laudo chegam juntos.

Identificadores de negócio (aliases — 1.1.0)

Campos opcionais que substituem o *_id correspondente. Todos resolvidos pelo gateway antes do envio ao backend. Detalhes de precedência e erros em Resolução por identificadores de negócio.

CampoTipoObrigatórioDescrição
paciente_cpfstring (11 díg.)nãoResolve paciente_id. Match exato em cnpj_cpf.
procedimento_codigo_tussstringnãoResolve procedimento_id. Match exato em codigo_tuss.
procedimento_abreviacaostringnãoResolve procedimento_id. Match exato em abreviacao.
convenio_codigo_ansstringnãoResolve convenio_id. Match exato em codigo_ans (só casa convênios com ANS preenchido).
convenio_nomestringnãoResolve convenio_id. Match exato em nome.
medico_exame_crmstringnãoResolve medico_exame_id. Match exato em crm (papel EXAME).
medico_laudo_crmstringnãoResolve medico_laudo_id. Match exato em crm (papel LAUDO).
unidade_nomestringnãoResolve unidade_id. Match exato em nome.

Campos bloqueados

Os campos exame_bloqueado, valor_repasse, valor_custo, software, imprimir, criado_por, medico_filtro_id e _via_api_chave_id são rejeitados com 400 VAL_INVALID_PARAM (a sentinela de origem é sempre definida pelo gateway).

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
201Created. Headers: Location: /v1/exames/{id}, Idempotency-Replay: false.
400VAL_INVALID_PARAMIdempotency-Key ausente/inválida, campo obrigatório faltando e sem default no tenant, formato inválido (ex.: CPF invalido. Informe 11 digitos.), extensão não permitida ou campo bloqueado enviado. Carrega details.lista[] que ensina (cada item com fonte/alias_aceito) — ver nota abaixo.
400VAL_FILE_TOO_LARGEArquivo inline acima de 20 MB.
403AUTH_INSUFFICIENT_SCOPEChave sem exames:write.
409CONCURRENT_REQUESTMesma Idempotency-Key em andamento. Retry-After: 2.
422IDEMPOTENCY_CONFLICTMesma key + body diferente.
422PARAM_CONFLICT*_id e o alias da mesma FK apontam para registros diferentes (ver resolução).
422RESOLUTION_NOT_FOUNDAlias enviado não casou nenhum registro exato.
422RESOLUTION_AMBIGUOUSAlias casou 2+ registros (resposta traz candidatos[], máx. 10).
422VALIDATION_ERRORValidação do backend (ex.: médico laudante obrigatório não informado).
422INVALID_REFERENCEPaciente/médico/procedimento/convênio inexistente.
422REPORT_ALREADY_EXISTSExame já possui laudo (imutabilidade).
503SERVICE_UNAVAILABLEFalha de comunicação com o backend; resultado cacheado sob a mesma key.
400 (validação) vs 422 (resolução). Os dois caminhos de erro acionável usam o mesmo envelope que ensina (details.field + details.lista[] com field/msg e enriquecimento fonte/alias_aceito), diferindo só no status. O 400 VAL_INVALID_PARAM cobre formato inválido (CPF, enum, campo bloqueado) e campo obrigatório omitido sem default no tenant — neste caso a details.lista[] traz, por campo faltante, a fonte (ex.: GET /v1/unidades) e o alias_aceito (ex.: unidade_nome), ensinando como completar o body. Já o 422 é a falha semântica de resolução de alias (RESOLUTION_NOT_FOUND / RESOLUTION_AMBIGUOUS / PARAM_CONFLICT) — ver a tabela 400 vs 422 nos códigos de erro.
{
  "data": null,
  "meta": { "version": "1.1.0", "timestamp": "2026-06-19T10:00:00-03:00" },
  "error": {
    "code": "VAL_INVALID_PARAM",
    "message": "2 campos precisam de atencao.",
    "details": {
      "field": "unidade_id",
      "lista": [
        { "field": "unidade_id", "msg": "Obrigatorio (sem default no tenant).",
          "fonte": "GET /v1/unidades", "alias_aceito": "unidade_nome" },
        { "field": "medico_exame_id", "msg": "Obrigatorio (sem default no tenant).",
          "fonte": "GET /v1/medicos?papel=EXAME", "alias_aceito": "medico_exame_crm" }
      ]
    }
  }
}

Resolução por identificadores de negócio

A partir de 1.1.0, em vez de descobrir as FKs nos catálogos, o cliente pode enviar identificadores de negócio (CPF, código TUSS, nome do convênio, CRM…). O gateway os resolve para o *_id interno chamando os search do backend do tenant (o gateway nunca faz SQL), com reconferência por igualdade exata normalizada. Os 8 aliases são opcionais e aditivos: os *_id continuam válidos (retrocompat total).

Alias (body)FK resolvidaFonte (backend search)Match exato em
paciente_cpfpaciente_idpacientes/searchcnpj_cpf (11 díg.)
procedimento_codigo_tussprocedimento_idprocedimentos/searchcodigo_tuss
procedimento_abreviacaoprocedimento_idprocedimentos/searchabreviacao
convenio_codigo_ansconvenio_idconvenios/searchcodigo_ans ¹
convenio_nomeconvenio_idconvenios/searchnome
medico_exame_crmmedico_exame_idmedicos/search (EXAME)crm
medico_laudo_crmmedico_laudo_idmedicos/search (LAUDO)crm
unidade_nomeunidade_idunidades/searchnome

¹ convenio_codigo_ans só casa convênios com o ANS preenchido — um registro sem ANS nunca resolve por esse alias.

Precedência (regra de ouro: não adivinhar)

  1. O *_id vence. Se o cliente envia o *_id (> 0) e um alias da mesma FK que resolve para um id diferente422 PARAM_CONFLICT. Um alias que não resolve (not_found/ambiguous/inválido) ao lado de um id explícito é ignorado (o backend valida no put).
  2. Sem *_id, com alias → resolve via search: 0 exatos → 422 RESOLUTION_NOT_FOUND; 2+ exatos → 422 RESOLUTION_AMBIGUOUS (com candidatos[], máx. 10); 1 exato → injeta o *_id.
  3. Sem *_id e sem alias → segue para os defaults do tenant e a validação de obrigatórios.

Quando uma FK tem 2 aliases (procedimento, convênio), o primeiro presente na ordem da tabela vence; resolvida a FK, os aliases seguintes da mesma FK são ignorados. Um alias mal formado (ex.: CPF com tamanho diferente de 11 dígitos) retorna 400 VAL_INVALID_PARAM com a mensagem CPF invalido. Informe 11 digitos. — a classe de formato tem precedência sobre os 422.

Defaults por tenant

Quando unidade_id, medico_exame_id, medico_laudo_id ou convenio_id são omitidos (ausentes/0 e não resolvidos por alias), o gateway aplica o default configurado no tenant (lido do backend uma vez por requisição). Defaults não sobrescrevem um valor já presente (do cliente ou de um alias) — por isso um payload legado com os *_id ignora os defaults (retrocompat). Um campo obrigatório com mecanismo de default mas sem default no tenant retorna 400 VAL_INVALID_PARAM (envelope M4 que ensina: details.lista[] com fonte + alias_aceito) com a mensagem Obrigatorio (sem default no tenant). (na prática só unidade_id / medico_exame_id). Use GET /v1/config para descobrir quais campos o tenant permite omitir.

Exemplo cURL — body mínimo (aliases + defaults)

Com defaults de tenant para unidade e médicos, este body basta para criar o exame:

BODY='{
  "paciente_cpf": "12345678900",
  "procedimento_abreviacao": "CAMPI",
  "convenio_nome": "Unimed"
}'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/exames
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/exames"

Exemplo cURL — legado (retrocompat, com *_id)

BODY='{
  "paciente_id": 1167,
  "unidade_id": 1,
  "procedimento_id": 4,
  "medico_exame_id": 1093,
  "medico_laudo_id": 1093,
  "referencia": "Campimetria OD/OE",
  "prioridade": 1
}'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/exames
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/exames"

Exemplo de resposta (201)

HTTP/1.1 201 Created
Location: /v1/exames/8421
Idempotency-Key: 2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f
Idempotency-Replay: false
X-Api-Version: v1
X-Api-Release: 1.1.0
X-Request-Id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
Content-Type: application/json; charset=utf-8

O data espelha a whitelist do GET /v1/exames/{id} (18 campos). Quando houve resolução por alias e/ou aplicação de defaults, o meta traz dois nós distintos: meta.resolvido (ids inferidos de alias, presente só se algum alias resolveu) e meta.defaults_aplicados (ids vindos de default do tenant, presente só se aplicou algum).

{
  "data": {
    "id": 8421,
    "paciente":     { "id": 1167, "nome": "JOAO DA SILVA" },
    "medico_exame": { "id": 1093, "nome": "Dra. Beatriz" },
    "medico_laudo": { "id": 1093, "nome": "Dra. Beatriz" },
    "solicitante":  null,
    "tipo_exame":   { "id": 4, "nome": "Campimetria" },
    "data_realizado": "2026-06-16",
    "status_laudo": "PENDENTE",
    "tem_arquivo_exame": false,
    "tem_arquivo_laudo": false,
    "url_arquivo_exame": null,
    "url_arquivo_laudo": null,
    "requer_laudo": false,
    "prioridade": 1,
    "criado_em": "2026-06-16"
  },
  "meta": {
    "version": "1.1.0",
    "request_id": "8b4f1c3a2e6d9b78",
    "duracao_ms": 512,
    "idempotent_replay": false
  },
  "error": null
}

Exemplo de meta (201, criado só com aliases + defaults)

Quando o body mínimo é resolvido por aliases e completado por defaults, o meta documenta a origem de cada id:

"meta": {
  "version": "1.1.0",
  "timestamp": "2026-06-19T10:00:00-03:00",
  "request_id": "1a2b3c4d5e6f7890",
  "duracao_ms": 412,
  "idempotent_replay": false,
  "resolvido": { "paciente_id": 1167, "procedimento_id": 4, "convenio_id": 31 },
  "defaults_aplicados": { "unidade_id": 1, "medico_exame_id": 1093, "medico_laudo_id": 1093 }
}

Exemplo de erro — campos não resolvidos (422)

Erros de resolução são consolidados (não aborta no primeiro). O code primário segue a severidade PARAM_CONFLICT > RESOLUTION_AMBIGUOUS > RESOLUTION_NOT_FOUND; a message é a do único erro, ou N campos precisam de atencao. quando há vários. Cada item de details.lista[] carrega field, code, msg e, conforme o caso, candidatos[] (ambiguous), id_informado/id_resolvido (conflict), além do enriquecimento fonte (onde descobrir o valor) e alias_aceito (o alias que evita mandar o *_id). O atalho details.candidatos no topo só aparece quando algum item é RESOLUTION_AMBIGUOUS.

{
  "data": null,
  "meta": { "version": "1.1.0", "request_id": "1a2b3c4d5e6f7901", "duracao_ms": 120 },
  "error": {
    "code": "RESOLUTION_AMBIGUOUS",
    "message": "2 campos precisam de atencao.",
    "details": {
      "field": "convenio_nome",
      "lista": [
        { "field": "convenio_nome", "code": "RESOLUTION_AMBIGUOUS",
          "msg": "Encontrado mais de um convenio com o mesmo nome.",
          "candidatos": [ { "id": 31, "nome": "UNIMED" }, { "id": 88, "nome": "UNIMED" } ],
          "fonte": "GET /v1/convenios", "alias_aceito": "convenio_nome" },
        { "field": "medico_exame_crm", "code": "RESOLUTION_NOT_FOUND",
          "msg": "Medico de exame nao localizado para o CRM informado.",
          "fonte": "GET /v1/medicos?papel=EXAME", "alias_aceito": "medico_exame_crm" }
      ],
      "candidatos": [ { "id": 31, "nome": "UNIMED" }, { "id": 88, "nome": "UNIMED" } ]
    }
  }
}

Try it out → ↑ Topo

POST /v1/exames/{id}/laudo — Ingerir laudo já assinado

Idempotency-Key obrigatório. Envie o header Idempotency-Key (UUID v4 recomendado, TTL 24h). Sem ele → 400 VAL_INVALID_PARAM. Mesma key + mesmo body → resposta cacheada (201 replay). Ver Idempotência.
O laudo deve estar JÁ ASSINADO. A API não assina, não gera PDF e não trafega senha médica nem certificado — ela apenas armazena o PDF recebido e cria o registro estruturado marcado como assinado (mesmo padrão da Central de Laudos).
Domínio — imutabilidade. Um exame aceita apenas 1 laudo. Se o exame já possui laudo, a chamada retorna 422 REPORT_ALREADY_EXISTS — o laudo nunca é sobrescrito.

O {id} do path é o exame alvo. O PDF do laudo vai inline em base64 em arquivo.base64. Após a ingestão, o laudo aparece em GET /v1/laudos (tem_arquivo: true) e o PDF fica baixável em GET /v1/laudos/{idLaudo}/arquivo.

Escopo exigido

laudos:write

Headers obrigatórios

Os 3 headers comuns (HMAC) + Content-Type: application/json + Idempotency-Key.

Body (JSON)

CampoTipoObrigatórioDescrição
medico_laudo_idintegersimMédico responsável pelo laudo (precisa ter permissão de laudar).
arquivo.nomestringsimNome do PDF (ex.: laudo-assinado.pdf).
arquivo.base64stringsimConteúdo do PDF assinado em base64. Somente PDF. Cap 20 MB. Aceita prefixo data URI.
titulostringnãoTítulo do laudo. Default: referencia ou "Laudo".
referenciastringnãoReferência/título livre.
cid_idintegernãoFK do CID (catálogo /v1/cids); resolvido para o código.

Campos bloqueados

rascunho, assinado_digitalmente, medico_senha, certificado, tipo_assinatura, exames_id (vem do path) e _via_api_chave_id são rejeitados com 400 VAL_INVALID_PARAM. O laudo é sempre armazenado como assinado; o cliente não escolhe rascunho nem fluxo de assinatura.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
201Created. Headers: Location: /v1/laudos/{idLaudo}, Idempotency-Replay: false.
400VAL_INVALID_PARAMIdempotency-Key ausente/inválida, campo obrigatório faltando, arquivo não-PDF, arquivo > 20 MB ou campo bloqueado enviado.
403AUTH_INSUFFICIENT_SCOPEChave sem laudos:write.
404RES_NOT_FOUNDExame inexistente.
409CONCURRENT_REQUESTMesma Idempotency-Key em andamento. Retry-After: 2.
422REPORT_ALREADY_EXISTSExame já possui laudo (imutabilidade) — nunca sobrescreve.
422INVALID_REFERENCEMédico inexistente ou sem permissão para laudar.
422VALIDATION_ERRORValidação genérica do backend.
503SERVICE_UNAVAILABLEFalha de comunicação com o backend; resultado cacheado sob a mesma key.

Exemplo cURL

BODY='{
  "medico_laudo_id": 1093,
  "titulo": "Laudo de Campimetria OD/OE",
  "arquivo": {
    "nome": "laudo-assinado.pdf",
    "base64": "JVBERi0xLjQKJ..."
  }
}'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/exames/8421/laudo
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/exames/8421/laudo"

Exemplo de resposta (201)

HTTP/1.1 201 Created
Location: /v1/laudos/532
Idempotency-Key: 7c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f
Idempotency-Replay: false
X-Api-Version: v1
Content-Type: application/json; charset=utf-8
{
  "data": {
    "id": 532,
    "exame": {
      "id": 8421,
      "paciente":   { "id": 1167, "nome": "JOAO DA SILVA" },
      "tipo_exame": { "id": 4, "nome": "Campimetria" }
    },
    "medico_laudo": { "id": 1093, "nome": "Dra. Beatriz" },
    "data_laudo": "2026-06-16",
    "hora_laudo": "14:32:10",
    "assinado": true,
    "assinado_em": "2026-06-16T17:32:10Z",
    "assinatura": { "tipo": "ICP-BRASIL", "hash": null, "validacao_url": null },
    "tem_arquivo": true,
    "url_arquivo": "/v1/laudos/532/arquivo",
    "criado_em": "2026-06-16T17:32:10Z",
    "atualizado_em": null
  },
  "meta": {
    "request_id": "9c5f2d4b3e7a1c80",
    "duracao_ms": 438,
    "idempotent_replay": false
  },
  "error": null
}

Exemplo de erro — imutabilidade (422)

{
  "data": null,
  "meta": { "request_id": "1a2b3c4d5e6f7900", "duracao_ms": 33 },
  "error": {
    "code": "REPORT_ALREADY_EXISTS",
    "message": "Exame já possui laudo cadastrado, não é possível criar um novo!",
    "details": null
  }
}

Try it out → ↑ Topo

GET /v1/laudos — Listar laudos

Listagem paginada de laudos do tenant da chave, com auto-filtro AMPLIADO de médico. Laudos órfãos (vínculo de exame nulo) aparecem com exame: null.

Escopo exigido

laudos:read

Parâmetros (query, todos opcionais)

NomeTipoDefaultObrigatórioDescrição
exame_id integer > 0 não FK laudo.idexamelaudoexames.exameid.
paciente_id integer > 0 não Via JOIN com exames (não é coluna de laudo).
medico_laudo_id integer > 0 não Ignorado se a chave tem medico_id (auto-filtro vence).
assinado 0 ou 1 não Whitelist estrita; mapeia laudo.assinado_digitalmente.
periodo_laudo_inicio YYYY-MM-DD não Inclusivo (laudo.datalaudo::DATE).
periodo_laudo_fim YYYY-MM-DD não Inclusivo.
limit integer 1–200 50 não Teto 200.
offset integer ≥ 0 0 não

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
400VAL_INVALID_PARAMParam fora da whitelist
401AUTH_*Falha de autenticação
403AUTH_INSUFFICIENT_SCOPEChave sem laudos:read
405METHOD_NOT_ALLOWEDOutro método que não GET

Schema de resposta (200)

CampoTipoDescrição
data[].idintegerPK do laudo (laudo.idlaudo)
data[].exameobjeto ExameRef ou null{id, paciente, tipo_exame}; null em laudo órfão
data[].exame.idintegerFK idexamelaudo
data[].exame.pacienteobjeto {id, nome}
data[].exame.tipo_exameobjeto {id, nome}
data[].medico_laudoobjeto {id, nome} ou nullMédico laudante
data[].data_laudoYYYY-MM-DDData do laudo
data[].hora_laudoHH:MM:SS ou null
data[].assinadobooleanMapeia laudo.assinado_digitalmente
data[].assinado_emISO 8601 UTC ou nullTimestamp ICP, quando aplicável
data[].tem_arquivobooleantrue se PDF do laudo anexado
data[].criado_emISO 8601 UTC
meta.limit/offset/total/has_moreinteger/booleanPaginação padrão

Campos excluídos por LGPD/segurança

  • descricaolaudo, descricaolaudo2 — texto/HTML do laudo (acesso somente via PDF em /v1/laudos/{id}/arquivo).
  • arquivolaudo — path S3 cru.
  • cid — código clínico sensível.
  • justificativa, rascunho — campos livres internos.
  • Hash, certificado emissor e data_assinatura — não persistidos no schema (decisão de produto, ver detalhe do laudo).

Notas e regras especiais

Auto-filtro AMPLIADO (§8)

Quando a chave tem medico_id = m, o predicado SQL aplicado é:

laudo.medicolaudo = m
  OR
EXISTS (SELECT 1 FROM exames
        WHERE exames.exameid = laudo.idexamelaudo
          AND (medicoexame = m
               OR medicolaudo = m
               OR medicosolicitante = m))

Ou seja: laudo é visível se o médico-laudo é o da chave, ou se o exame vinculado satisfaz a regra SIMPLES. O parâmetro medico_laudo_id da query é ignorado silenciosamente.

Laudo órfão

Quando laudo.idexamelaudo é nulo ou aponta para exame deletado, o campo exame vem como null no payload. Comportamento documentado e esperado — pode aparecer normalmente em listagens históricas.

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/laudos?assinado=1&limit=5"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -s \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": [{
    "id": 516,
    "exame": {
      "id": 99,
      "paciente":   { "id": 1133, "nome": "JOAO TESTE" },
      "tipo_exame": { "id": 7,    "nome": "CAMPO VISUAL" }
    },
    "medico_laudo": { "id": 1093, "nome": "Dra. Beatriz" },
    "data_laudo":   "2026-03-24",
    "hora_laudo":   "20:28:45",
    "assinado":     false,
    "assinado_em":  null,
    "tem_arquivo":  true,
    "criado_em":    "2026-03-24T20:28:45Z"
  }],
  "meta": {
    "limit": 5, "offset": 0,
    "total": 90, "has_more": true,
    "request_id": "...", "duracao_ms": 71
  },
  "error": null
}

Try it out → ↑ Topo

GET /v1/laudos/{id} — Detalhe do laudo

Retorna dados completos do laudo, com sub-objeto assinatura quando aplicável. Auto-filtro AMPLIADO aplicado.

Escopo exigido

laudos:read

Parâmetros (path)

NomeTipoObrigatórioDescrição
id integer > 0 sim PK do laudo (laudo.idlaudo).

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
400VAL_INVALID_PARAMid não é inteiro positivo
401AUTH_*Falha de autenticação
403AUTH_INSUFFICIENT_SCOPEChave sem laudos:read
404RES_NOT_FOUNDID inexistente ou oculto pelo auto-filtro AMPLIADO

Schema de resposta (200)

CampoTipoDescrição
data.idintegerPK
data.exameobjeto ExameRef ou null{id, paciente, tipo_exame}; null em laudo órfão
data.medico_laudoobjeto {id, nome} ou null
data.data_laudoYYYY-MM-DD
data.hora_laudoHH:MM:SS ou null
data.assinadoboolean
data.assinado_emISO 8601 UTC ou nullPreenchido apenas se assinado=true
data.assinaturaobjeto {tipo} ou nullApenas tipo (ex.: "ICP-BRASIL"); null quando não assinado
data.tem_arquivoboolean
data.url_arquivostring (URL relativa) ou nullEx.: /v1/laudos/516/arquivo
data.criado_emISO 8601 UTC
data.atualizado_emISO 8601 UTC ou null

Campos excluídos por LGPD/segurança

Mesma lista de listar laudos. Em especial:

  • descricaolaudo / descricaolaudo2 — texto/HTML do laudo só pelo PDF.
  • arquivolaudo — path S3 cru.
  • Hash da assinatura, certificado emissor, URL ITI de validação — não persistidos no schema (decisão de produto / DOMINIO 2.3).

Notas e regras especiais

Imutabilidade do laudo assinado: quando assinado=true, o laudo torna-se imutável. Endpoints de escrita futuros recusarão qualquer modificação. Para corrigir um laudo assinado, será necessário criar uma nova versão vinculada ao mesmo exame_id.

O sub-objeto assinatura carrega apenas tipo. Para verificar a assinatura, baixe o PDF (que contém a assinatura embarcada) e use ferramenta padrão (Adobe Reader, validador ITI).

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/laudos/516"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -s \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta — laudo não assinado (200)

{
  "data": {
    "id": 516,
    "exame": {
      "id": 99,
      "paciente":   { "id": 1133, "nome": "JOAO TESTE" },
      "tipo_exame": { "id": 7,    "nome": "CAMPO VISUAL" }
    },
    "medico_laudo": { "id": 1093, "nome": "Dra. Beatriz" },
    "data_laudo":   "2026-03-24",
    "hora_laudo":   "20:28:45",
    "assinado":     false,
    "assinado_em":  null,
    "assinatura":   null,
    "tem_arquivo":  true,
    "url_arquivo":  "/v1/laudos/516/arquivo",
    "criado_em":    "2026-03-24T20:28:45Z",
    "atualizado_em": null
  },
  "meta": { "request_id": "...", "duracao_ms": 64 },
  "error": null
}

Exemplo de resposta — laudo assinado (200)

{
  "data": {
    "id": 516,
    "exame": { "id": 99, "paciente": {"id":1133,"nome":"JOAO TESTE"},
               "tipo_exame": {"id":7,"nome":"CAMPO VISUAL"} },
    "medico_laudo": { "id": 1093, "nome": "Dra. Beatriz" },
    "data_laudo":   "2026-03-24",
    "hora_laudo":   "20:28:45",
    "assinado":     true,
    "assinado_em":  "2026-03-24T20:28:45Z",
    "assinatura":   { "tipo": "ICP-BRASIL" },
    "tem_arquivo":  true,
    "url_arquivo":  "/v1/laudos/516/arquivo",
    "criado_em":    "2026-03-24T20:28:45Z",
    "atualizado_em": null
  },
  "meta": { "request_id": "...", "duracao_ms": 66 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/laudos/{id}/arquivo — PDF do laudo (302 S3)

Retorna 302 Found com header Location: apontando para presigned S3 válida por 300s. Diferente de /v1/exames/{id}/arquivo, laudo tem apenas 1 PDF (sem ?tipo=).

Escopo exigido

laudos:read

Parâmetros (path)

NomeTipoObrigatórioDescrição
id integer > 0 sim PK do laudo.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
302Redirect para presigned S3
401AUTH_*Falha de autenticação
403AUTH_INSUFFICIENT_SCOPEChave sem laudos:read
404RES_NOT_FOUNDLaudo inexistente ou oculto pelo auto-filtro AMPLIADO
404RES_ARQUIVO_NAO_DISPONIVELLaudo órfão ou PDF ainda não gerado
500RES_ARQUIVO_S3_FALHAFalha temporária ao gerar presigned

Schema de resposta (302)

CampoTipoDescrição
Header Locationstring (URL S3)Presigned com X-Amz-Expires=300
Bodyvazio

Campos excluídos por LGPD/segurança

Path S3 cru nunca aparece em logs ou na resposta. O PDF está fisicamente em exames.arquivolaudo (folder S3 laudos/) — o JOIN é feito internamente pela API.

Notas e regras especiais

  • Laudo órfão (sem idexamelaudo válido) → 404 RES_ARQUIVO_NAO_DISPONIVEL.
  • Mesmas regras de redirect/cache do /v1/exames/{id}/arquivo: não cacheie a URL S3; respeite a expiração de 300s; em PHP cURL, não reassine com seu HMAC.
  • O PDF de laudo assinado contém a assinatura ICP embarcada — valide com Adobe Reader ou validador ITI.

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/laudos/516/arquivo"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -L -o laudo-516.pdf \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (302)

HTTP/1.1 302 Found
Location: https://sivoe4.s3.sa-east-1.amazonaws.com/laudos/relatorio-516.pdf
         ?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=300&X-Amz-Signature=...
X-Api-Version: v1
X-Content-Type-Options: nosniff
Cache-Control: no-store

Try it out → ↑ Topo

GET /v1/agenda/agendamentos — Agendamentos de um paciente

Caso de uso primário: portal do paciente externo. Um integrador (sistema RealClinic-like) busca os agendamentos do paciente que ele atende e exibe em app/portal de terceiro. Por isso paciente_id é obrigatório — mitigação anti-dump da agenda do tenant.

Escopo exigido

agenda:read

Parâmetros (query)

NomeTipoDefaultObrigatórioDescrição
paciente_id inteiro > 0 sim Sem ele → 400 VAL_INVALID_PARAM com details.field=paciente_id.
data_inicio YYYY-MM-DD não Inclusivo (data_agendamento >= data_inicio).
data_fim YYYY-MM-DD não Inclusivo.
status string não Whitelist (8 valores): agendado, aguardando_confirmacao, confirmado, checkin, em_atendimento, finalizado, cancelado, nao_compareceu.
medico_id inteiro > 0 não Ignorado silenciosamente se a chave tem medico_id (auto-filtro vence — não retorna 403, evita enumeração).
unidade_id inteiro > 0 não Sem filtro → todas as unidades do tenant.
agenda_config_id inteiro > 0 não Filtra por agenda específica.
limit integer (1–200) 50 não Teto absoluto 200.
offset integer (≥ 0) 0 não Paginação.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK (lista paginada)
400VAL_INVALID_PARAMpaciente_id ausente, status fora da whitelist, datas inválidas, limit > 200.
403AUTH_INSUFFICIENT_SCOPEChave sem agenda:read.
405METHOD_NOT_ALLOWEDVerbo não suportado.

Schema de resposta (200)

CampoTipoDescrição
data[].idintegerID do agendamento.
data[].pacienteobject{ id, nome } — sem PII adicional.
data[].medicoobject{ id, nome }.
data[].salaobject | null{ id, nome } quando aplicável.
data[].unidadeobject{ id, nome }.
data[].procedimentoobject{ id, nome }.
data[].agenda_config_idintegerFK agenda_config.id.
data[].data_agendamentoYYYY-MM-DDData do agendamento.
data[].hora_inicioHH:MMHora de início.
data[].hora_fimHH:MMHora de fim — inferida pelo backend na criação.
data[].statusstringUm dos 8 valores da whitelist.
data[].encaixebooleantrue quando o agendamento é encaixe fora da grade.
data[].cancelado_emISO 8601 | nullCarimbo do cancelamento, se aplicável.
data[].criado_emISO 8601Carimbo de criação.
metaobject{ limit, offset, total, has_more, request_id, duracao_ms }.

Campos excluídos (defesa em profundidade LGPD)

  • paciente_celular, paciente_cpf, paciente_nascimento, paciente_sexo, paciente_email — snapshots PII gravados em agenda_agendamentos não são expostos pela API pública. Use GET /v1/pacientes/{id} com escopo pacientes:read separado.
  • cancelado_motivo, cancelado_por, observacoes — texto interno (operacional/auditoria).
  • exame_id — vínculo interno com a tabela legada de exames.

Comportamentos importantes

  • Agendamentos com paciente_id NULL (paciente registrado apenas por nome livre, ainda não cadastrado no sistema) não aparecem na listagem pública. Filtro aplicado pelo modelo.
  • Agendamentos cancelados aparecem na listagem (são histórico). Para filtrar, use ?status=....
  • Auto-filtro de médico SIMPLES: quando a chave tem medico_id, apenas agendamentos cuja agenda_config.medico_id = m são visíveis. O parâmetro medico_id da query é ignorado silenciosamente.

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/agenda/agendamentos?paciente_id=1133&data_inicio=2026-05-01&limit=50"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": [{
    "id": 5001,
    "paciente":     { "id": 1133, "nome": "JOAO DA SILVA" },
    "medico":       { "id": 1093, "nome": "Dra. Beatriz" },
    "sala":         null,
    "unidade":      { "id": 1, "nome": "Matriz" },
    "procedimento": { "id": 7, "nome": "CAMPO VISUAL" },
    "agenda_config_id": 17,
    "data_agendamento": "2026-05-20",
    "hora_inicio": "09:00",
    "hora_fim": "09:30",
    "status": "confirmado",
    "encaixe": false,
    "cancelado_em": null,
    "criado_em": "2026-05-15T10:42:00Z"
  }],
  "meta": { "limit": 50, "offset": 0, "total": 3, "has_more": false, "request_id": "a1b2c3d4e5f6", "duracao_ms": 91 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/agenda/agendamentos/{id} — Detalhe + histórico de status

Detalhe completo de um agendamento, incluindo histórico cronológico de transições de status. O usuario_id de cada entrada do histórico é informação interna de auditoria e não é exposto.

Escopo exigido

agenda:read

Path param

NomeTipoDescrição
idinteiro > 0ID do agendamento.

Auto-filtro de médico

Agendamento que não pertence ao médico da chave retorna 404 RES_NOT_FOUND (mesma mensagem que ID inexistente — não vaza existência).

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
400VAL_INVALID_PARAMID inválido (não inteiro positivo).
403AUTH_INSUFFICIENT_SCOPEChave sem agenda:read.
404RES_NOT_FOUNDID inexistente OU auto-filtro bloqueia.

Schema do histórico

Array data.historico ordenado cronologicamente (mais antigo primeiro):

CampoTipoDescrição
status_anteriorstringStatus antes da transição (whitelist).
status_novostringStatus após a transição (whitelist).
data_horaISO 8601 UTCCarimbo da transição.
observacaostring | nullTexto livre da operação. usuario_id NÃO exposto.

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/agenda/agendamentos/5001"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": {
    "id": 5001,
    "paciente":     { "id": 1133, "nome": "JOAO DA SILVA" },
    "medico":       { "id": 1093, "nome": "Dra. Beatriz" },
    "sala":         null,
    "unidade":      { "id": 1, "nome": "Matriz" },
    "procedimento": { "id": 7, "nome": "CAMPO VISUAL" },
    "agenda_config_id": 17,
    "data_agendamento": "2026-05-20",
    "hora_inicio": "09:00",
    "hora_fim": "09:30",
    "status": "confirmado",
    "encaixe": false,
    "cancelado_em": null,
    "criado_em": "2026-05-15T10:42:00Z",
    "historico": [
      { "status_anterior": "agendado",                "status_novo": "aguardando_confirmacao", "data_hora": "2026-05-19T07:00:00Z", "observacao": "Notificacao automatica D-1" },
      { "status_anterior": "aguardando_confirmacao", "status_novo": "confirmado",            "data_hora": "2026-05-19T14:22:00Z", "observacao": "Paciente confirmou por SMS" }
    ]
  },
  "meta": { "request_id": "...", "duracao_ms": 73 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/agenda/slots — Próximos slots livres por procedimento

Para um procedimento_id dado, retorna o próximo slot livre por médico/sala dentro do horizonte. Encaixes não bloqueiam o grid (mesma regra do calendário visual interno).

Escopo exigido

agenda:read

Parâmetros (query)

NomeTipoDefaultObrigatórioDescrição
procedimento_id inteiro > 0 sim FK exame.id — dicionário compartilhado entre tenants.
unidade_id inteiro > 0 não Restringe agendas elegíveis.
horizonte_dias integer (1–180) 60 não Backend valida o teto máximo (180). Acima → 400.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
400VAL_INVALID_PARAMprocedimento_id ausente, horizonte_dias > 180.
403AUTH_INSUFFICIENT_SCOPEChave sem agenda:read.
405METHOD_NOT_ALLOWEDVerbo não suportado.

Schema de resposta (200)

Cada item de data[] é um grupo (combinação médico + sala + unidade). Ordenação: por proximo_slot.data ASC; nulos no final, ordenados por label.

CampoTipoDescrição
data[].medico_idinteger0 quando o vínculo é por sala.
data[].sala_idinteger0 quando o vínculo é por médico.
data[].is_medicobooleantrue = grupo por médico; false = por sala.
data[].labelstringNome de exibição (montado a partir de saudacao + nome).
data[].unidade_idintegerFK da unidade.
data[].proximo_slotobject | null{ data, hora_inicio, hora_fim, agenda_config_id } ou null.

Campos excluídos

  • medico_key — chave interna do backend (composição), nunca exposta.
  • Detalhes sensíveis do médico: CRM, CPF/CNPJ — só nome de exibição.

Auto-filtro

Quando a chave tem medico_id, somente configs com agenda_config.medico_id = m aparecem (SIMPLES). O parâmetro medico_id da query é ignorado silenciosamente.

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/agenda/slots?procedimento_id=4&horizonte_dias=30"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": [
    { "medico_id": 42, "sala_id": 0, "is_medico": true, "label": "Dr. Joao Garcia", "unidade_id": 1,
      "proximo_slot": { "data": "2026-05-19", "hora_inicio": "09:00", "hora_fim": "09:30", "agenda_config_id": 17 } },
    { "medico_id": 0, "sala_id": 3, "is_medico": false, "label": "Sala Exame", "unidade_id": 1,
      "proximo_slot": null }
  ],
  "meta": { "total": 2, "request_id": "...", "duracao_ms": 156 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/agenda/configs — Listar agendas configuradas

Retorna apenas metadados da agenda. Procedimentos vinculados, horários, restrições e relacionamentos N:N permanecem internos (sem detalhe individual nesta sub-entrega).

Escopo exigido

agenda:read

Parâmetros (query, todos opcionais)

NomeTipoDefaultDescrição
medico_idinteiro > 0Ignorado se a chave tem medico_id.
sala_idinteiro > 0Filtra apenas configs com tipo_vinculo=sala.
unidade_idinteiro > 0Filtra agenda_config.unidade_id.
tipo_vinculomedico | salaWhitelist estrita.
ativo0 | 1Whitelist estrita. Outro valor → 400.
nomestring (≥ 3)ILIKE %nome%, mínimo 3 chars após trim.
limitinteger (1–200)50Teto 200.
offsetinteger (≥ 0)0Paginação.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK
400VAL_INVALID_PARAMWhitelist violada (ativo, tipo_vinculo), limit > 200.
403AUTH_INSUFFICIENT_SCOPEChave sem agenda:read.
405METHOD_NOT_ALLOWEDVerbo não suportado.

Campos excluídos

  • duplicado_de — id interno de origem de duplicação.
  • criado_por — identificador interno do operador que criou o registro.
  • atualizado_em — omitido em listas (foco em estado atual).
  • procedimentos[] — relação N:N interna.
  • horarios[], restricoes[], restricao_idade — ainda não há endpoint público de detalhe.

Exemplo cURL

TS=$(date +%s)
PATH_Q="/v1/agenda/configs?ativo=1&limit=50"
PAYLOAD="${TS}.GET ${PATH_Q}
"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  "https://api.sivoe.med.br${PATH_Q}"

Exemplo de resposta (200)

{
  "data": [{
    "id": 17,
    "nome": "AGENDA CAMPO VISUAL — DRA. BEATRIZ",
    "tipo_vinculo": "medico",
    "medico":  { "id": 1093, "nome": "Dra. Beatriz" },
    "sala":    null,
    "unidade": { "id": 1, "nome": "Matriz" },
    "ativo": true,
    "data_inicio": "2026-01-01",
    "data_fim":    null,
    "criado_em":   null
  }],
  "meta": { "limit": 50, "offset": 0, "total": 2, "has_more": false, "request_id": "...", "duracao_ms": 88 },
  "error": null
}

Try it out → ↑ Topo

POST /v1/agenda/agendamentos — Criar agendamento

Idempotency-Key obrigatório. Todos os POSTs de agenda exigem o header Idempotency-Key (UUID v4 recomendado, TTL 24h). Sem ele → 400 VAL_INVALID_PARAM. Ver contrato completo na seção Idempotência.

Cria um novo agendamento. Aceita paciente já cadastrado (paciente_id) ou snapshot por nome livre (paciente_nome + paciente_celular obrigatórios). Esta é a rota principal de integração externa para escrita de agenda.

Escopo exigido

agenda:write

Headers obrigatórios

Os 3 headers comuns + Content-Type: application/json + Idempotency-Key.

Body (JSON)

CampoTipoObrigatórioDescrição
agenda_config_idintegersimFK agenda_config.id.
procedimento_idintegersimFK exame.id.
convenio_idintegersim0 = particular.
dataYYYY-MM-DDsimData do agendamento.
hora_inicioHH:MMsimhora_fim NÃO é enviada — backend infere automaticamente a partir da duração do procedimento.
paciente_idintegercondicionalQuando ausente, paciente_nome + paciente_celular obrigatórios. paciente_id vence sobre o snapshot quando ambos vierem.
paciente_nomestring (1–200)condicionalObrigatório quando paciente_id ausente.
paciente_celularstringcondicionalObrigatório quando paciente_id ausente.
paciente_cpfstring (11 dígitos)nãoSnapshot — gravado em agenda_agendamentos mas nunca exposto em GET.
paciente_nascimentoYYYY-MM-DDnãoSnapshot.
paciente_sexoM | F | OnãoSnapshot. Whitelist estrita.
paciente_emailstringnãoSnapshot.
plano_idintegernãoPlano do convênio.
encaixebooleannãoDefault false. true = agendamento fora da grade.
observacoesstringnãoMáx. mb_strlen ≤ 500.
unidade_idintegernãoQuando ausente, herda a unidade configurada na agenda.

hora_fim inferida pelo backend

O cliente envia apenas hora_inicio. A duração vem de agenda_config_procedimentos.tempo_minutos para a combinação agenda_config_id × procedimento_id. Se o procedimento não estiver cadastrado nessa agenda, a resposta é 422 BUSINESS_RULE com mensagem "Procedimento sem duração configurada para esta agenda."

force=true e ignorar_conflito=true bloqueados

Override de validação não é permitido por API pública. Esses campos não são simplesmente ignorados — são rejeitados com 400 VAL_INVALID_PARAM e details.field=force (ou field=ignorar_conflito).

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
201Created. Headers: Location: /v1/agenda/agendamentos/{id}, Idempotency-Replay: false.
400VAL_INVALID_PARAMIdempotency-Key ausente / inválida, campos obrigatórios faltando, formato inválido, force=true, ignorar_conflito=true.
403AUTH_INSUFFICIENT_SCOPEChave sem agenda:write.
404RES_NOT_FOUNDAuto-filtro de médico bloqueia agenda alheia.
409CONCURRENT_REQUESTMesma Idempotency-Key em flight. Retry-After: 2.
422IDEMPOTENCY_CONFLICTMesma key + body diferente.
422CONFLICTHorário ocupado (outro agendamento no mesmo médico/data/hora).
422DUPLICATEPaciente já agendado no mesmo dia/hora.
422BUSINESS_RULEConvênio bloqueado, limite atingido, idade fora do escopo, ou procedimento sem duração configurada (sem isso o backend não consegue inferir a hora_fim).

Exemplo cURL

BODY='{
  "agenda_config_id": 17,
  "procedimento_id": 4,
  "convenio_id": 0,
  "data": "2026-06-10",
  "hora_inicio": "10:30",
  "paciente_id": 1167,
  "encaixe": false,
  "observacoes": "Retorno de campimetria."
}'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/agenda/agendamentos
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/agenda/agendamentos"

Exemplo de resposta (201)

HTTP/1.1 201 Created
Location: /v1/agenda/agendamentos/8421
Idempotency-Key: 2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f
Idempotency-Replay: false
X-Api-Version: v1
X-Request-Id: 1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d
Content-Type: application/json; charset=utf-8
{
  "data": {
    "id": 8421,
    "paciente":     { "id": 1167, "nome": "JOAO DA SILVA" },
    "medico":       { "id": 1093, "nome": "Dra. Beatriz" },
    "sala":         null,
    "unidade":      { "id": 1, "nome": "Matriz" },
    "procedimento": { "id": 4, "nome": "Campimetria" },
    "agenda_config_id": 17,
    "data_agendamento": "2026-06-10",
    "hora_inicio": "10:30",
    "hora_fim": "11:00",
    "status": "agendado",
    "encaixe": false,
    "criado_em": "2026-05-19T18:30:00Z"
  },
  "meta": {
    "request_id": "8b4f1c3a2e6d9b78",
    "duracao_ms": 412,
    "idempotent_replay": false
  },
  "error": null
}

Try it out → ↑ Topo

POST /v1/agenda/agendamentos/{id}/cancelar — Cancelar agendamento

Idempotency-Key obrigatório. Sem ele → 400 VAL_INVALID_PARAM. Ver Idempotência.

Cancela um agendamento existente preservando o histórico (cancelamento NÃO apaga a entrada em histórico do agendamento — direito do titular preservado para rastreabilidade legal, ver LGPD em POSTs).

Escopo exigido

agenda:write

Headers obrigatórios

3 headers comuns + Content-Type: application/json + Idempotency-Key.

Path param

NomeTipoDescrição
idinteiro > 0ID do agendamento.

Body (JSON)

CampoTipoObrigatórioDescrição
motivostringsimmb_strlen 3–500. Fora do range → 400 VAL_INVALID_PARAM.
forceBLOQUEADO. Presença → 400 VAL_INVALID_PARAM details.field=force.

Auto-filtro de médico SIMPLES estendido

O auto-filtro SIMPLES dos endpoints de leitura (medico_id da chave = agenda_config.medico_id) é estendido para os POSTs: a chave do médico A não pode cancelar agendamento cuja agenda_config.medico_id aponta para o médico B. A resposta é 404 RES_NOT_FOUND — mesma mensagem que ID inexistente, para não vazar existência.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK (idempotent_replay=false na 1ª vez).
400VAL_INVALID_PARAMIdempotency-Key ausente/inválida, motivo curto/longo/ausente, force=true.
403AUTH_INSUFFICIENT_SCOPEChave sem agenda:write.
404RES_NOT_FOUNDID inexistente OU auto-filtro de médico bloqueia.
409CONCURRENT_REQUESTMesma key em flight. Retry-After: 2.
422INVALID_STATEAgendamento já cancelado ou finalizado.
422BUSINESS_RULESem permissão de unidade (modo única).

Replay do cancelar

  • Cancelar a mesma id duas vezes com Idempotency-Key diferente: 1ª devolve 200, 2ª devolve 422 INVALID_STATE.
  • Com Idempotency-Key igual: 2ª devolve 200 replay (idempotent_replay=true) — não re-executa.

Exemplo cURL

BODY='{ "motivo": "Paciente solicitou cancelamento via WhatsApp" }'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/agenda/agendamentos/8421/cancelar
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/agenda/agendamentos/8421/cancelar"

Exemplo de resposta (200)

{
  "data": {
    "id": 8421,
    "status": "cancelado",
    "cancelado_em": "2026-05-19T18:45:32Z",
    "cancelado_motivo": "Paciente solicitou cancelamento via WhatsApp",
    "data_agendamento": "2026-06-10",
    "hora_inicio": "10:30",
    "paciente": { "id": 1167, "nome": "JOAO DA SILVA" }
  },
  "meta": { "request_id": "...", "duracao_ms": 184, "idempotent_replay": false },
  "error": null
}

Try it out → ↑ Topo

POST /v1/agenda/agendamentos/{id}/reagendar — Reagendar (id preservado)

Idempotency-Key obrigatório. Sem ele → 400 VAL_INVALID_PARAM. Ver Idempotência.

Reagenda um agendamento existente preservando o id (o backend faz UPDATE no registro original em vez de criar novo + cancelar antigo). O cliente continua referenciando o mesmo identificador.

Escopo exigido

agenda:write

Headers obrigatórios

3 headers comuns + Content-Type: application/json + Idempotency-Key.

Path param

NomeTipoDescrição
idinteiro > 0ID do agendamento (PRESERVADO na resposta).

Body (JSON)

CampoTipoObrigatórioDescrição
nova_dataYYYY-MM-DDsimNova data.
nova_hora_inicioHH:MMsimNova hora de início. hora_fim é inferida pelo backend.
agenda_config_idintegernãoQuando ausente, mantém a agenda atual.
motivostringnãoQuando presente, ocupa reagendado_motivo.
forceBLOQUEADO.
ignorar_conflitoBLOQUEADO.

hora_fim inferida pelo backend

Mesma regra do criar — backend calcula a partir da nova combinação agenda_config_id × procedimento_id. Procedimento sem duração configurada na agenda alvo → 422 BUSINESS_RULE.

Auto-filtro estendido

A chave do médico A não pode reagendar o agendamento do médico B nem direcionar o reagendamento para uma agenda_config de B. Ambos os lados (o registro original e a config alvo) precisam pertencer ao médico da chave; caso contrário → 404 RES_NOT_FOUND.

Histórico mantém apenas a versão atual

O backend grava 1 única entrada de histórico do reagendamento (status_anterior → 'agendado' + observação com data/hora anteriores). O reagendamento é atômico: o registro original é atualizado in-place, preservando o id e evitando entradas fantasmas de cancelamento intermediário.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200OK (id preservado).
400VAL_INVALID_PARAMDatas/horário inválidos, Idempotency-Key ausente, force / ignorar_conflito.
403AUTH_INSUFFICIENT_SCOPEChave sem agenda:write.
404RES_NOT_FOUNDID inexistente OU auto-filtro de médico bloqueia — em ambos os lados.
409CONCURRENT_REQUESTMesma key em flight.
422CONFLICTNovo horário ocupado.
422INVALID_STATECancelado/finalizado não pode ser reagendado.
422BUSINESS_RULEProcedimento sem duração, convênio bloqueado, restrição de unidade.

Exemplo cURL

BODY='{
  "nova_data": "2026-06-12",
  "nova_hora_inicio": "14:30",
  "motivo": "Medico precisou remarcar"
}'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/agenda/agendamentos/8421/reagendar
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/agenda/agendamentos/8421/reagendar"

Exemplo de resposta (200 — id preservado)

{
  "data": {
    "id": 8421,
    "paciente":     { "id": 1167, "nome": "JOAO DA SILVA" },
    "medico":       { "id": 1093, "nome": "Dra. Beatriz" },
    "sala":         null,
    "unidade":      { "id": 1, "nome": "Matriz" },
    "procedimento": { "id": 4, "nome": "Campimetria" },
    "data_agendamento": "2026-06-12",
    "hora_inicio": "14:30",
    "hora_fim": "15:00",
    "status": "agendado",
    "encaixe": false,
    "reagendado_em": "2026-05-19T19:01:12Z",
    "reagendado_motivo": "Medico precisou remarcar"
  },
  "meta": { "request_id": "...", "duracao_ms": 503, "idempotent_replay": false },
  "error": null
}

Try it out → ↑ Topo

POST /v1/pacientes/{id}/prontuarios — Criar prontuário (mensagem e/ou anexos)

Idempotency-Key obrigatório. Envie o header Idempotency-Key (UUID v4 recomendado, TTL 24h). Sem ele → 400 VAL_INVALID_PARAM. Mesma key + mesmo body → resposta cacheada (201 replay). Ver Idempotência.

O {id} do path é o paciente. Um prontuário é uma mensagem (HTML) e/ou um ou mais anexos. É obrigatório informar pelo menos um dos dois. Os anexos são vinculados ao paciente (não a um prontuário específico) e aparecem em GET .../prontuarios/anexos.

Visibilidade. exibir: 1 (default) marca o prontuário como destinado ao paciente (visível no portal). O registro é sempre gravado finalizado (rascunho = 0) — a via API não cria rascunhos.

Escopo exigido

prontuarios:write

Headers obrigatórios

Os 3 headers comuns (HMAC) + Content-Type: application/json + Idempotency-Key.

Body (JSON)

CampoTipoObrigatórioDescrição
mensagemstring (HTML)condicionalTexto/HTML do prontuário. Conta como mensagem com mais de 3 caracteres. Obrigatório se não houver anexos.
anexosarraycondicionalLista de anexos. Obrigatório se não houver mensagem.
anexos[].nomestringsimNome do arquivo (ex.: exame.pdf). A extensão define o tipo.
anexos[].base64stringsimConteúdo em base64. PDF, PNG ou JPG. Cap 20 MB. Aceita prefixo data URI.
anexos[].descricaostringnãoLegenda do anexo. Default: o nome.
exibirinteger (0|1)nãoVisível ao paciente. Default 1.

Campos bloqueados

rascunho, _via_api_chave_id, paciente_id (vem do path), id, unidade_id e tipo são rejeitados com 400 VAL_INVALID_PARAM.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
201Created. Headers: Location: /v1/pacientes/{id}/prontuarios, Idempotency-Replay: false.
400VAL_INVALID_PARAMIdempotency-Key ausente/inválida, nem mensagem nem anexo, anexo inválido/> 20 MB, extensão não permitida ou campo bloqueado.
403AUTH_INSUFFICIENT_SCOPEChave sem prontuarios:write.
404RES_NOT_FOUNDPaciente inexistente.
409CONCURRENT_REQUESTMesma Idempotency-Key em andamento. Retry-After: 2.
422IDEMPOTENCY_CONFLICTMesma key com body diferente.
422VALIDATION_ERRORValidação do backend (anexo/arquivo inválido).
503SERVICE_UNAVAILABLEFalha de comunicação com o backend; resultado cacheado sob a mesma key.

Exemplo cURL

BODY='{
  "mensagem": "<p>Paciente em acompanhamento.</p>",
  "exibir": 1,
  "anexos": [
    { "nome": "exame.pdf", "base64": "JVBERi0xLjQK...", "descricao": "Exame laboratorial" }
  ]
}'
TS=$(date +%s)
PAYLOAD="${TS}.POST /v1/pacientes/1167/prontuarios
${BODY}"
SIG=$(printf '%s' "$PAYLOAD" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')

curl -i -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  --data-binary "$BODY" \
  "https://api.sivoe.med.br/v1/pacientes/1167/prontuarios"

Exemplo de resposta (201)

{
  "data": {
    "prontuario_id": 884,
    "anexos": [
      { "id": 1201, "descricao": "Exame laboratorial", "extensao": "PDF", "tamanho": 184320 }
    ]
  },
  "meta": { "request_id": "...", "duracao_ms": 712, "idempotent_replay": false },
  "error": null
}

Try it out → ↑ Topo

GET /v1/pacientes/{id}/prontuarios — Listar prontuários (mensagens)

Lista paginada dos prontuários (mensagens) do paciente. Retorna todos — não filtra por exibir. O conteúdo de anexos não vem aqui; use os endpoints de anexos abaixo.

Escopo exigido

prontuarios:read

Query string

ParâmetroTipoDescrição
limitintegerDefault 50, máx. 200.
offsetintegerDeslocamento de paginação. Default 0.

Campos retornados (por item)

CampoTipoDescrição
idintegerID do prontuário.
data_emissaostringData/hora de emissão.
mensagemstring (HTML)Conteúdo do prontuário.
exibirbooleanVisível ao paciente.
rascunhobooleanRascunho (via API sempre false).
medicoobject|null{ id, nome } do autor, quando houver.

Exemplo de resposta (200)

{
  "data": [
    {
      "id": 884,
      "data_emissao": "2026-06-16 10:42:00",
      "mensagem": "<p>Paciente em acompanhamento.</p>",
      "exibir": true,
      "rascunho": false,
      "medico": { "id": 1093, "nome": "Dra. Beatriz" }
    }
  ],
  "meta": { "limit": 50, "offset": 0, "total": 1, "has_more": false, "request_id": "...", "duracao_ms": 88 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/pacientes/{id}/prontuarios/anexos — Metadados dos anexos

Nunca expõe URL nem chave S3. Esta listagem devolve apenas metadados. Para baixar um anexo, peça a URL no endpoint dedicado .../anexos/{anexoId}/url (302 presigned).

Lista os metadados de todos os anexos de prontuário do paciente. Os anexos são por paciente.

Escopo exigido

prontuarios:read

Campos retornados (por item)

CampoTipoDescrição
idintegerID do anexo (use no endpoint de URL).
descricaostringLegenda do anexo.
extensaostringEx.: PDF, PNG, JPG.
tamanhointegerTamanho em bytes.

Exemplo de resposta (200)

{
  "data": [
    { "id": 1201, "descricao": "Exame laboratorial", "extensao": "PDF", "tamanho": 184320 }
  ],
  "meta": { "total": 1, "request_id": "...", "duracao_ms": 61 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/pacientes/{id}/prontuarios/anexos/{anexoId}/url — URL do anexo (302 presigned)

Anti-IDOR. O backend valida que o anexo pertence ao paciente {id} do path. Anexo de outro paciente (ou inexistente) → 404 RES_NOT_FOUND — sem vazar existência.

Responde 302 com Location apontando para a URL pré-assinada do S3 (validade curta). O conteúdo nunca passa pelo gateway. Cache-Control: no-store.

Escopo exigido

prontuarios:read

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
302Redirect para a presigned S3. Location + Cache-Control: no-store.
400VAL_INVALID_PARAMid/anexoId não inteiro positivo.
403AUTH_INSUFFICIENT_SCOPEChave sem prontuarios:read.
404RES_NOT_FOUNDAnexo inexistente ou de outro paciente (anti-IDOR).
503SERVICE_UNAVAILABLEBackend não retornou a URL.

Exemplo de resposta (302)

HTTP/1.1 302 Found
Location: https://s3.amazonaws.com/...&X-Amz-Signature=...
Cache-Control: no-store
X-Api-Version: v1

Try it out → ↑ Topo

GET /v1/procedimentos — Catálogo de procedimentos

Lista os procedimentos (exames-catálogo) da clínica. Use o id retornado para preencher procedimento_id ao criar um exame (POST /v1/exames). Sem auto-filtro de médico — uma chave com medico_id ainda vê todos. Cache-Control: private, max-age=300.

Escopo exigido

catalogos:read

Parâmetros (query, todos opcionais)

ParamTipoDescrição
qstringPrefixo (LIKE) no nome. Mínimo 2 chars.
ativointeger (0/1)Default 1 (somente ativos).
limit / offsetintegerPaginação (limit 1–200, default 50).

Campos retornados (por item)

CampoTipoDescrição
idintegerID do procedimento (use em procedimento_id).
nomestringNome do procedimento.
abreviacaostringSigla curta.
codigo_tussstringCódigo TUSS (vazio se não cadastrado).
descricao_tussstringDescrição TUSS.
requer_laudobooleantrue = procedimento gera laudo médico.
possui_imagensbooleanProcedimento de imagem.
ativobooleanAtivo no tenant.

Exemplo de resposta (200)

{
  "data": [
    { "id": 17, "nome": "ELETROCARDIOGRAMA", "abreviacao": "ECG",
      "codigo_tuss": "40101010", "descricao_tuss": "ELETROCARDIOGRAMA CONVENCIONAL DE 12 DERIVACOES",
      "requer_laudo": true, "possui_imagens": false, "ativo": true }
  ],
  "meta": { "version": "1.0.0", "timestamp": "2026-06-17T08:41:48-03:00", "limit": 50, "offset": 0 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/convenios — Convênios com planos aninhados

Catálogo de convênios com os planos aninhados em planos[], em uma única chamada (sem N+1). Use convenio.id e plano.id ao montar o sub-objeto convenio de um paciente. Cache-Control: private, max-age=300.

Escopo exigido

catalogos:read

Parâmetros (query, todos opcionais)

ParamTipoDescrição
qstringPrefixo (LIKE) no nome do convênio. Mínimo 2 chars.
ativointeger (0/1)Aceito por simetria (schema legado não faz soft-delete de convênio).
limit / offsetintegerPaginação.

Campos retornados (por item)

CampoTipoDescrição
idintegerID do convênio.
nomestringNome do convênio.
abreviacaostringSigla curta.
codigo_ansstringVazio na v1 (schema legado não armazena).
ativobooleanAtivo no tenant.
planosarraySempre presente (pode ser vazio). Cada plano: { id, nome, codigo_ans, ativo }.

Exemplo de resposta (200)

{
  "data": [
    { "id": 3, "nome": "UNIMED", "abreviacao": "UNI", "codigo_ans": "", "ativo": true,
      "planos": [ { "id": 11, "nome": "BASICO", "codigo_ans": "", "ativo": true } ] }
  ],
  "meta": { "version": "1.0.0", "timestamp": "2026-06-17T08:41:48-03:00", "limit": 50, "offset": 0 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/medicos — Médicos (filtro por papel)

LGPD reforçada. Médico é pessoa física — a whitelist bloqueia cpf, rg, telefones, e-mail pessoal, endereço, data_nascimento, login, senha, certificado e foto.

Catálogo de médicos com filtro opcional por papel. Use o id para preencher medico_exame_id / medico_laudo_id / solicitante nas escritas. Sem auto-filtro — necessário para compor os papéis no POST /v1/exames. Cache-Control: private, max-age=300.

Escopo exigido

catalogos:read

Parâmetros (query, todos opcionais)

ParamTipoDescrição
papelstring (multi)EXAME · LAUDO · SOLICITANTE. Multi-valor = união (OR): ?papel=EXAME&papel=LAUDO.
qstringPrefixo (LIKE) no nome. Mínimo 3 chars.
ativointeger (0/1)Default 1.
limit / offsetintegerPaginação.

Campos retornados (por item)

CampoTipoDescrição
idintegerID do médico.
nomestringNome do médico.
saudacaostringTratamento (ex.: DRA).
conselhoobjeto{ sigla, numero, uf } — ex.: CRM 84122 MG.
especialidadestringEspecialidade principal.
papeisarrayPapéis ativos (ex.: ["EXAME","LAUDO"]).
ativobooleanAtivo no tenant.

Exemplo de resposta (200)

{
  "data": [
    { "id": 1093, "nome": "MARIA WAGNER", "saudacao": "DRA",
      "conselho": { "sigla": "CRM", "numero": "84122", "uf": "MG" },
      "especialidade": "CARDIOLOGIA", "papeis": ["EXAME","LAUDO"], "ativo": true }
  ],
  "meta": { "version": "1.0.0", "timestamp": "2026-06-17T08:41:48-03:00", "limit": 50, "offset": 0 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/medicos/{id} — Detalhe do médico

LGPD. cpf e rg vêm mascarados (apenas os 2 últimos dígitos). senha, foto e certificado nunca são expostos.

Detalhe de um médico. saudacao_id é o campo confiável da saudação (o texto saudacao pode vir vazio no detalhe). Retorna 404 RES_NOT_FOUND se o id não existir.

Escopo exigido

catalogos:read

Exemplo de resposta (200)

{
  "data": {
    "id": 1093, "nome": "CARLOS EDUARDO SILVA", "nome_registro": "",
    "saudacao": "DR.", "saudacao_id": 3,
    "cpf": "***.***.***-01", "rg": "*****12", "data_nascimento": "1980-05-20",
    "sexo": "MAS",
    "conselho": { "tipo": "CRM", "numero": "123456" },
    "papeis": ["LAUDO","SOLICITANTE"], "grupo_permissao_id": 4,
    "usuario": "csilva",
    "telefone": "1133334444", "celular": "11988887777", "email": "csilva@clinica.com.br",
    "endereco": { "cep": "01310930", "logradouro": "AV PAULISTA", "numero": "1000",
                  "complemento": "SALA 12", "bairro": "BELA VISTA",
                  "cidade": "SAO PAULO", "estado": "SP" },
    "cor_agenda": "#3366cc", "ativo": true
  },
  "meta": { "version": "1.5.0", "timestamp": "2026-07-06T09:41:48-03:00" },
  "error": null
}

Try it out → ↑ Topo

POST /v1/medicos — Criar médico

Idempotency-Key obrigatório. O médico é um usuário do sistema: usuario e senha são obrigatórios.

saudacao e grupo_permissao aceitam texto (resolvido no gateway) ou os ids (saudacao_id, grupo_permissao_id). cidade/estado do endereço são resolvidos por nome no backend. Retorna 201 com header Location e o detalhe (cpf/rg mascarados).

Escopo exigido

medicos:write

Campos do corpo

CampoObrig.Descrição
nomesim3–100 caracteres.
saudacao / saudacao_idsimTexto (ex.: Dr.) ou id. Ambíguo/inexistente → 422 com sugestoes/candidatos.
grupo_permissao / grupo_permissao_idsimTexto (ex.: MEDICO) ou id.
papeissim≥1 de EXAME · LAUDO · SOLICITANTE · SOLICITANTE_EXTERNO.
usuariosimLogin de acesso (≥3; A-Z a-z 0-9 . _ - @).
senhasimWrite-only (≥3), nunca retornada.
conselhonão{ tipo, numero } (ex.: CRM 123456).
cpfnãoCPF (11) ou CNPJ (14) dígitos.
rg, data_nascimento, sexonãosexo = MAS · FEM · OUT.
telefone, celular, emailnãoContatos.
endereconão{ cep, logradouro, numero, complemento, bairro, cidade, estado }.
cor_agenda, ativonãocor_agenda = #rrggbb.
Atenção. Médico criado com o papel SOLICITANTE_EXTERNO fica invisível ao GET/listagem (limitação do backend). Combine com outro papel se precisar recuperá-lo depois.

Exemplo de corpo

{
  "nome": "CARLOS EDUARDO SILVA",
  "saudacao": "Dr.",
  "grupo_permissao": "MEDICO",
  "papeis": ["LAUDO", "SOLICITANTE"],
  "usuario": "csilva",
  "senha": "Trocar@123",
  "conselho": { "tipo": "CRM", "numero": "123456" },
  "cpf": "12345678901",
  "email": "csilva@clinica.com.br",
  "endereco": { "cidade": "SAO PAULO", "estado": "SP", "cep": "01310930",
                "logradouro": "AV PAULISTA", "numero": "1000" },
  "ativo": true
}

Try it out → ↑ Topo

PATCH /v1/medicos/{id} — Atualizar médico (parcial)

Idempotency-Key obrigatório. Merge parcial (GET-merge-PUT): campo omitido não é tocado.

papeis, conselho e endereco substituem o objeto/lista inteiros (sem sub-merge). usuario/senha só mudam se enviados (omiti-los preserva o acesso atual). Duplicidade de usuario/cpf/email422. Retorna 200 com o detalhe atualizado (cpf/rg mascarados).

Escopo exigido

medicos:write

Exemplo de corpo

{
  "email": "carlos.novo@clinica.com.br",
  "papeis": ["EXAME", "LAUDO", "SOLICITANTE"],
  "conselho": { "tipo": "CRM", "numero": "654321" },
  "cor_agenda": "#0d6efd"
}

Try it out → ↑ Topo

GET /v1/cids — Busca CID-10

q é obrigatório (mínimo 2 chars). A API nunca devolve o dump dos ~14 mil CIDs. O limit tem cap mais restrito (100) que os demais catálogos.

Busca CID-10 por código (prefixo) OU descrição (contém). Cache-Control: private, max-age=300.

Escopo exigido

catalogos:read

Parâmetros (query)

ParamTipoDescrição
q (obrigatório)stringMatch em código (LIKE 'q%') OU descrição (LIKE '%q%'). Mínimo 2 chars.
limitinteger1–100, default 50 (cap mais restrito).
offsetintegerPaginação.

Campos retornados (por item)

CampoTipoDescrição
idintegerID do CID.
codigostringCódigo CID (ex.: A09, K59.0).
descricaostringDescrição do CID.
ativobooleanAtivo.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200Resultado da busca (lista, pode ser vazia).
400VAL_INVALID_PARAMq ausente ou com menos de 2 chars.
403AUTH_INSUFFICIENT_SCOPEChave sem catalogos:read.

Exemplo de resposta (200)

{
  "data": [
    { "id": 312, "codigo": "A09", "descricao": "DIARREIA E GASTROENTERITE DE ORIGEM INFECCIOSA PRESUMIVEL", "ativo": true }
  ],
  "meta": { "version": "1.0.0", "timestamp": "2026-06-17T08:41:48-03:00", "limit": 50, "offset": 0 },
  "error": null
}

Try it out → ↑ Topo

GET /v1/unidades — Catálogo de unidades

Esta é a fonte para descobrir o unidade_id/unidade_nome usado no POST /v1/exames (alias unidade_nome). A whitelist de saída é estrita: apenas id e nome.

Lista as unidades (clínicas/filiais) da clínica. Sem auto-filtro de médico — a chave vê todas as unidades. Cache-Control: private, max-age=300.

Escopo exigido

catalogos:read

Parâmetros (query)

ParamTipoDescrição
limitinteger1–200, default 50.
offsetintegerPaginação. Sem filtro q (tenants têm poucas unidades).

Campos retornados (por item)

CampoTipoDescrição
idintegerID da unidade.
nomestringNome da unidade (use como unidade_nome).

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200Lista de unidades (pode ser vazia).
403AUTH_INSUFFICIENT_SCOPEChave sem catalogos:read.
405METHOD_NOT_ALLOWEDMétodo diferente de GET.

Exemplo de resposta (200)

{
  "data": [
    { "id": 1, "nome": "ROOMTEC1-A" },
    { "id": 5, "nome": "ROOMTEC2-B" },
    { "id": 9, "nome": "ROOMTEC3-C" }
  ],
  "meta": { "version": "1.1.0", "timestamp": "2026-06-19T08:41:48-03:00", "limit": 50, "offset": 0, "has_more": false },
  "error": null
}

Try it out → ↑ Topo

GET /v1/complementos — Catálogo de complementos

Esta é a fonte para descobrir o complementos_id (campo opcional do POST /v1/exames). A whitelist de saída é estrita: apenas id e nome.

Lista os complementos de exame da clínica. Complementos são globais ao tenant — não há vínculo por procedimento (sem filtro procedimento_id). Sem auto-filtro de médico. Cache-Control: private, max-age=300.

Escopo exigido

catalogos:read

Parâmetros (query)

ParamTipoDescrição
limitinteger1–200, default 50.
offsetintegerPaginação. Sem filtro q (tenants têm poucos complementos).

Campos retornados (por item)

CampoTipoDescrição
idintegerID do complemento (use como complementos_id).
nomestringNome do complemento.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200Lista de complementos (pode ser vazia).
403AUTH_INSUFFICIENT_SCOPEChave sem catalogos:read.
405METHOD_NOT_ALLOWEDMétodo diferente de GET.

Exemplo de resposta (200)

{
  "data": [
    { "id": 3, "nome": "URGENTE" },
    { "id": 7, "nome": "DOMICILIAR" }
  ],
  "meta": { "version": "1.1.0", "timestamp": "2026-06-19T08:41:48-03:00", "limit": 50, "offset": 0, "has_more": false },
  "error": null
}

Try it out → ↑ Topo

GET /v1/grupos-laudos — Catálogo de grupos de laudo

Esta é a fonte para descobrir o grupos_laudos_id (campo opcional do POST /v1/exames, usado em laudo por produção). A whitelist de saída é estrita: apenas id e nome — a composição de médicos do grupo não é exposta.

Lista os grupos de médicos laudantes da clínica. Sem auto-filtro de médico. Cache-Control: private, max-age=300.

Escopo exigido

catalogos:read

Parâmetros (query)

ParamTipoDescrição
limitinteger1–200, default 50.
offsetintegerPaginação. Sem filtro q (tenants têm poucos grupos).

Campos retornados (por item)

CampoTipoDescrição
idintegerID do grupo (use como grupos_laudos_id).
nomestringNome do grupo de laudo.

Códigos de resposta possíveis

HTTPerror.codeQuando ocorre
200Lista de grupos de laudo (pode ser vazia).
403AUTH_INSUFFICIENT_SCOPEChave sem catalogos:read.
405METHOD_NOT_ALLOWEDMétodo diferente de GET.

Exemplo de resposta (200)

{
  "data": [
    { "id": 2, "nome": "GRUPO CARDIOLOGIA" },
    { "id": 4, "nome": "GRUPO RADIOLOGIA" }
  ],
  "meta": { "version": "1.1.0", "timestamp": "2026-06-19T08:41:48-03:00", "limit": 50, "offset": 0, "has_more": false },
  "error": null
}

Try it out → ↑ Topo