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.
| Header | Formato | Notas |
|---|---|---|
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.
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
| Atributo | Valor |
|---|---|
| Header obrigatório | Idempotency-Key |
| Formato | 1–64 chars, regex ^[A-Za-z0-9_-]+$ |
| Recomendação | UUID 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 |
| Escopo | Por chave de API — a mesma key em chaves distintas não colide |
| Comportamento | Mesma 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. AguardeRetry-Aftersegundos 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:
- Verificar via
GET /v1/agenda/agendamentos?paciente_id=Xse o registro foi criado mesmo assim; ou - Gerar uma nova
Idempotency-Keye 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.
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]":
| Categoria | Chaves cobertas |
|---|---|
| Credenciais | senha, password, secret, token, usuario, api_key, api_secret, x_signature, authorization |
| Documentos | cpf, cnpj, rg, cns, cpf_responsavel, paciente_cpf, paciente_rg |
| Contato direto | celular, 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
| Dado | Retenção | Notas |
|---|---|---|
| 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.
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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
401 | AUTH_* | Falha de autenticação (header ausente, assinatura inválida, timestamp fora da janela…) |
Schema de resposta (200)
| Campo | Tipo | Descrição |
|---|---|---|
data.ok | boolean | Sempre true |
data.service | string | "Sivoe API Gateway" |
data.version | string | "v1" |
data.server_time | ISO 8601 UTC | Hora corrente do servidor |
data.backend_called | boolean | false em GET (não chamou backend) |
data.auth.api_key_masked | string | Chave parcialmente mascarada (ex.: svp_live_a1b2****c3d4) |
data.auth.chave_id | integer | ID interno da chave |
data.auth.clinica | string | Slug do tenant resolvido (ex.: sandbox) |
data.auth.medico_id | integer | 0 se a chave não tem auto-filtro; caso contrário, o medico_id vinculado |
data.auth.escopos | array<string> | Lista de escopos da chave |
data.echo.method | string | "GET" |
data.echo.body | null | Sempre 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
}
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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK (body ecoado em data.echo.body) |
401 | AUTH_* | Falha de autenticação |
413 | VAL_INVALID_PARAM | Body 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,passwordcpf,cns,rgtoken,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
}
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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
401 | AUTH_* | Falha de autenticação (header ausente, assinatura inválida, timestamp fora da janela…) |
Schema de resposta (200)
| Campo | Tipo | Descrição |
|---|---|---|
data.version | string | Versão do gateway (ex.: "1.1.0"). |
data.api_version | string | "v1" (contrato em vigor). |
data.escopos | array<string> | Escopos atribuídos à chave. |
data.enums.sexo | array<string> | Valores aceitos em sexo (POST/PATCH /v1/pacientes): MAS, FEM, OUT. |
data.enums.arquivo_tipo | array<string> | Valores do ?tipo= em GET /v1/exames/{id}/arquivo: exame, laudo. |
data.enums.status_laudo | array<string> | Filtro/saída de status de laudo: PENDENTE, EMITIDO, ASSINADO. |
data.enums.prioridade | array<integer> | Prioridade no POST /v1/exames: 1 (Normal), 2 (Alta), 3 (Urgente). |
data.enums.requer_laudo | array<integer> | Flag requer_laudo no POST /v1/exames: 0, 1. |
data.defaults.unidade_id | integer | null | Default da unidade; null = sem default. |
data.defaults.medico_exame_id | integer | null | Default do médico do exame; null = sem default. |
data.defaults.medico_laudo_id | integer | null | Default do médico do laudo; null = sem default. |
data.defaults.convenio_id | integer | null | Default 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
}
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)
| Nome | Tipo | Default | Obrigatório | Descriçã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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
400 | VAL_INVALID_PARAM | Param fora da whitelist (error.details.field indica qual) |
401 | AUTH_* | Falha de autenticação |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem pacientes:read |
405 | METHOD_NOT_ALLOWED | Outro método que não GET |
Schema de resposta (200)
| Campo | Tipo | Descrição |
|---|---|---|
data[].id | integer | PK do paciente |
data[].nome | string | Nome completo em CAIXA ALTA (padrão do legado) |
data[].cpf | string (11 dígitos) ou null | CPF sem formatação |
data[].data_nascimento | YYYY-MM-DD ou null | — |
data[].sexo | enum M / F ou null | — |
data[].telefone | string ou null | — |
data[].email | string ou null | — |
data[].ativo | boolean | — |
data[].criado_em | ISO 8601 UTC | — |
meta.limit | integer | Eco do limit aplicado |
meta.offset | integer | Eco do offset |
meta.total | integer | Total que satisfaz o filtro no tenant |
meta.has_more | boolean | true 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
}
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)
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id |
integer > 0 | sim | PK do paciente (usuarios.id no legado). |
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
400 | VAL_INVALID_PARAM | id não é inteiro positivo |
401 | AUTH_* | Falha de autenticação |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem pacientes:read |
404 | RES_NOT_FOUND | ID inexistente no tenant (mensagem genérica, sem details) |
Schema de resposta (200)
| Campo | Tipo | Descrição |
|---|---|---|
data.id | integer | PK |
data.saudacao | string | Ex.: "SR.", "SRA.", "DR." |
data.nome | string | Nome em caixa alta |
data.nome_registro | string | Nome social/registro (pode ser vazio) |
data.numero_prontuario | string | Identificador interno da clínica |
data.cpf | string ou null | 11 dígitos |
data.rg | string ou null | — |
data.data_nascimento | YYYY-MM-DD ou null | — |
data.sexo | M/F ou null | — |
data.estado_civil | objeto {id, nome} ou null | FK estado_civil |
data.telefone | string ou null | — |
data.celular | string ou null | — |
data.email | string ou null | — |
data.endereco | objeto Endereco | cep, logradouro, numero, complemento, bairro, cidade, estado |
data.convenio | objeto {id, nome, numero_carteirinha} ou null | Pode vir null se paciente sem convênio |
data.ativo | boolean | — |
data.criado_em | ISO 8601 UTC | — |
data.atualizado_em | ISO 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
}
POST
/v1/pacientes
— Criar paciente (Fase 3*)
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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
nome | string (5–100 chars) | sim | Nome completo. Backend grava em caixa alta (paridade com CMS). |
data_nascimento | YYYY-MM-DD | sim | Entre 130 anos atrás e hoje (inclusive). |
cpf | string (11 dígitos) | condicional | Obrigatório se cpf_obrigatorio=1 na getReferencias do tenant. Quando enviado: regex + algoritmo de dígito verificador (validação dupla: gateway + backend). |
celular | string | condicional | Obrigatório se celular_obrigatorio=1 na getReferencias. |
saudacao | string | não | Ex.: "Sr.", "Sra.", "Dr.". |
nome_registro | string | não | Nome social/registro civil. |
sexo | MAS | FEM | OUT | não | String. O backend legado aceita 1/2/3 no CMS, mas a API pública exige a string. |
rg | string | não | RG sem máscara obrigatória. |
estado_civil_id | integer | não | FK estado_civil.id. |
telefone | string | não | Telefone fixo (sem máscara). |
email | string | não | E-mail válido. |
numero_prontuario | string | não | Quando ausente, o backend auto-gera (resposta inclui o valor real). Quando enviado e já em uso por outro paciente → 422 MEDICAL_RECORD_ALREADY_EXISTS. |
endereco | objeto Endereco | não | Sub-objeto {cep, logradouro, numero, complemento, bairro, cidade_id, estado}. Presente substitui inteiro; ausente mantém vazio. |
convenio | objeto | não | Sub-objeto {id, plano_id, numero_carteirinha}. Presente substitui inteiro. |
ativo | boolean | não | Default backend (geralmente true no perfil PACIENTE). |
usuario | string (1–50 chars) | não | Usuá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. |
senha | string (1–50 chars) | não | Write-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_temporaria | boolean | não | Só 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
| HTTP | error.code | Quando ocorre |
|---|---|---|
201 | — | Created. Headers: Location: /v1/pacientes/{id}, Idempotency-Replay: false. |
400 | VAL_INVALID_PARAM | Idempotency-Key ausente/inválida, campos obrigatórios faltando, formato inválido (incl. CPF com dígito verificador errado), sexo fora de MAS|FEM|OUT. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem pacientes:write. |
409 | CONCURRENT_REQUEST | Mesma Idempotency-Key em flight. Retry-After: 2. |
422 | IDEMPOTENCY_CONFLICT | Mesma key + body diferente. |
422 | CPF_ALREADY_EXISTS | CPF 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. |
422 | MEDICAL_RECORD_ALREADY_EXISTS | numero_prontuario já em uso. details.paciente_id_existente + details.nome. |
422 | USUARIO_ALREADY_EXISTS | usuario 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. |
422 | BUSINESS_RULE | Validação do backend (ex.: campo obrigatório faltando por getReferencias do tenant). |
503 | SERVICE_UNAVAILABLE | Network 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*:
| Campo | Tipo | Descrição |
|---|---|---|
data.criado_em | ISO 8601 UTC | Timestamp do INSERT (migração 85 adicionou a coluna). |
data.atualizado_em | ISO 8601 UTC ou null | null em paciente recém-criado. |
data.criado_por_chave_id | integer ou null | ID da chave HMAC que criou o registro. null quando criado via CMS legado (paridade bit-idêntica preservada). |
data.atualizado_por_chave_id | integer ou null | null 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*.
PATCH
/v1/pacientes/{id}
— Atualizar paciente parcialmente (Fase 3*)
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
| Nome | Tipo | Descrição |
|---|---|---|
id | inteiro > 0 | PK 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": null— zera 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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK. id preservado. atualizado_em e atualizado_por_chave_id recebem valores; criado_em e criado_por_chave_id são preservados. |
400 | VAL_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.). |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem pacientes:write. |
404 | RES_NOT_FOUND | Paciente {id} não existe no tenant. |
409 | CONCURRENT_REQUEST | Mesma key em flight. |
422 | IDEMPOTENCY_CONFLICT | Mesma key + body diferente. |
422 | CPF_ALREADY_EXISTS | PATCH alterando CPF para CPF de outro paciente. PATCH com o mesmo CPF do próprio paciente → 200 OK idempotente. |
422 | MEDICAL_RECORD_ALREADY_EXISTS | PATCH alterando numero_prontuario para um já em uso. |
422 | USUARIO_ALREADY_EXISTS | PATCH redefinindo usuario para um já em uso por outro paciente (case-insensitive). Redefinir para o mesmo usuário do próprio paciente é idempotente. |
422 | BUSINESS_RULE | Validação do backend após merge. |
503 | SERVICE_UNAVAILABLE | Network 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
200replay (Idempotency-Replay: true). Body diferente →422 IDEMPOTENCY_CONFLICT. Network error é cacheado como503— retry da mesma key devolve 503 replay; o cliente deve verificar viaGET /v1/pacientes/{id}ou gerar nova key. - PATCH idempotente do próprio CPF: enviar
"cpf": "<cpf_atual>"no PATCH não gera422 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:cepenumeronormalizam"0"→""na resposta).
POST
/v1/pacientes/unificar
— Unificar pacientes duplicados
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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
paciente_id_principal | inteiro > 0 | sim | ID do paciente que SERÁ MANTIDO (destino). Recebe todas as referências do duplicado. |
paciente_id_duplicado | inteiro > 0 | sim | ID 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 ≤ 0 →
400 VAL_INVALID_PARAM.
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | Unificação concluída. Body { paciente_id_principal, paciente_id_duplicado, status: "unificado" }. Header Idempotency-Replay: false. |
400 | VAL_INVALID_PARAM | Idempotency-Key ausente/inválida, body não-JSON, ou ID ausente/não-inteiro/≤ 0. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem pacientes:write. |
409 | CONCURRENT_REQUEST | Mesma Idempotency-Key em flight. Retry-After: 2. |
422 | PACIENTES_UNIFICACAO_MESMO_ID | paciente_id_principal igual a paciente_id_duplicado — não faz sentido unificar um paciente consigo mesmo. |
422 | IDEMPOTENCY_CONFLICT | Mesma key + body diferente. |
422 | BUSINESS_RULE | Regra de negócio do backend: paciente não encontrado ou inválido. error.message traz a causa. |
503 | SERVICE_UNAVAILABLE | Backend indisponível. Cacheado por 24h — retry com mesma key devolve 503 (ver Idempotência). |
504 | SERVICE_TIMEOUT | Timeout 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".
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)
| Nome | Tipo | Default | Obrigatório | Descriçã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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
400 | VAL_INVALID_PARAM | Param fora da whitelist (error.details.field indica qual) |
401 | AUTH_* | Falha de autenticação |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem exames:read |
405 | METHOD_NOT_ALLOWED | Outro método que não GET |
Schema de resposta (200)
| Campo | Tipo | Descrição |
|---|---|---|
data[].id | integer | PK do exame (exames.exameid) |
data[].paciente | objeto {id, nome} | Sub-objeto compacto (PacienteRef) |
data[].medico_exame | objeto {id, nome} ou null | Médico responsável pela realização do exame |
data[].medico_laudo | objeto {id, nome} ou null | Médico responsável pelo laudo |
data[].solicitante | objeto {id, nome} ou null | Médico solicitante (pode ser externo) |
data[].tipo_exame | objeto {id, nome} | Ex.: {id:7, nome:"CAMPO VISUAL"} |
data[].data_realizado | YYYY-MM-DD | Data clínica do exame |
data[].status_laudo | enum PENDENTE/EMITIDO/ASSINADO | Derivado em SQL (ver Notas) |
data[].tem_arquivo_exame | boolean | true se PDF do exame anexado |
data[].tem_arquivo_laudo | boolean | true se PDF do laudo anexado |
data[].criado_em | ISO 8601 UTC | — |
meta.limit/offset/total/has_more | integer/boolean | Paginaçã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-agentde 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:
| Status | Condição | Estado clínico |
|---|---|---|
PENDENTE | arquivolaudo vazio/nulo | Laudo ainda não emitido |
EMITIDO | arquivolaudo preenchido e sem laudo.assinado_digitalmente = 1 para o exame | Laudo gerado mas não assinado |
ASSINADO | arquivolaudo preenchido e existe laudo com assinado_digitalmente = 1 | Laudo final com assinatura ICP-BRASIL |
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
}
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)
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id |
integer > 0 | sim | PK do exame (exames.exameid). |
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
400 | VAL_INVALID_PARAM | id não é inteiro positivo |
401 | AUTH_* | Falha de autenticação |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem exames:read |
404 | RES_NOT_FOUND | ID inexistente ou oculto pelo auto-filtro de médico |
Schema de resposta (200)
| Campo | Tipo | Descrição |
|---|---|---|
data.id | integer | PK |
data.paciente | objeto {id, nome} | — |
data.medico_exame | objeto {id, nome} ou null | — |
data.medico_laudo | objeto {id, nome} ou null | — |
data.solicitante | objeto {id, nome} ou null | — |
data.tipo_exame | objeto {id, nome} | — |
data.data_realizado | YYYY-MM-DD | — |
data.hora_realizado | HH:MM:SS ou null | — |
data.status_laudo | enum (ver listar exames) | Mesmo derivado SQL |
data.laudo_assinado_em | ISO 8601 UTC ou null | Timestamp da assinatura ICP, quando aplicável |
data.tem_arquivo_exame | boolean | — |
data.tem_arquivo_laudo | boolean | — |
data.url_arquivo_exame | string (URL relativa) ou null | Ex.: /v1/exames/99/arquivo?tipo=exame |
data.url_arquivo_laudo | string (URL relativa) ou null | Ex.: /v1/exames/99/arquivo?tipo=laudo |
data.criado_em | ISO 8601 UTC | — |
data.atualizado_em | ISO 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 (tabelaexame, semclinica).
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
}
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
| Local | Nome | Tipo | Obrigatório | Descriçã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
| HTTP | error.code | Quando ocorre |
|---|---|---|
302 | — | Redirect para presigned S3 (válida 300s) |
400 | VAL_INVALID_PARAM | tipo ausente ou fora da whitelist |
401 | AUTH_* | Falha de autenticação |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem exames:read |
404 | RES_NOT_FOUND | Exame inexistente ou oculto pelo auto-filtro |
404 | RES_ARQUIVO_NAO_DISPONIVEL | Recurso existe mas o PDF S3 ainda não foi anexado |
500 | RES_ARQUIVO_S3_FALHA | Falha temporária ao gerar presigned (retry com backoff) |
Schema de resposta (302)
| Campo | Tipo | Descrição |
|---|---|---|
Header Location | string (URL S3) | Presigned com X-Amz-Expires=300 |
| Body | vazio | — |
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
-Lpara seguir o redirect. PHP cURL: mantenhaCURLOPT_FOLLOWLOCATION = falsee 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
POST
/v1/exames
— Criar exame
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).
*_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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
paciente_id | integer | sim | FK do paciente cadastrado. Pode ser omitido enviando o alias paciente_cpf (ver resolução). |
unidade_id | integer | condicional | FK da unidade/clínica. Omitível se o tenant tiver default ou se enviar o alias unidade_nome. |
procedimento_id | integer | sim | FK do procedimento (tipo de exame). Pode ser omitido enviando o alias procedimento_codigo_tuss ou procedimento_abreviacao. |
medico_exame_id | integer | condicional | Médico responsável pelo exame. Omitível se o tenant tiver default ou se enviar o alias medico_exame_crm. |
medico_laudo_id | integer | condicional | Mé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_id | integer | condicional | Grupo de médicos laudantes (laudo por produção). Descubra em GET /v1/grupos-laudos. |
medico_solicitante_id | integer | condicional | Exigido quando a clínica marca solicitante como obrigatório. |
data_exame | YYYY-MM-DD | não | Default: data atual. |
numero | string | não | Número interno do exame. |
referencia | string | não | Referência livre. |
observacoes | string | não | Anamnese / observações. |
requer_laudo | 0 | 1 | não | 1 = exame requer laudo. |
cid_id | integer | não | FK do CID. |
complementos_id | integer | não | FK de complemento. Descubra em GET /v1/complementos. |
plano_id | integer | não | Plano do convênio. |
convenio_id | integer | não | Convênio. Omitível se o tenant tiver default ou via alias convenio_codigo_ans / convenio_nome. |
prioridade | 1 | 2 | 3 | não | 1 Normal, 2 Alta, 3 Urgente. Default 1. |
arquivos.exames[] | array {nome, base64} | não | Arquivo(s) do exame. Cap 20 MB/arquivo. PDF/PNG/JPG/JPEG. |
arquivos.laudos[] | array {nome, base64} | não | PDF(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.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
paciente_cpf | string (11 díg.) | não | Resolve paciente_id. Match exato em cnpj_cpf. |
procedimento_codigo_tuss | string | não | Resolve procedimento_id. Match exato em codigo_tuss. |
procedimento_abreviacao | string | não | Resolve procedimento_id. Match exato em abreviacao. |
convenio_codigo_ans | string | não | Resolve convenio_id. Match exato em codigo_ans (só casa convênios com ANS preenchido). |
convenio_nome | string | não | Resolve convenio_id. Match exato em nome. |
medico_exame_crm | string | não | Resolve medico_exame_id. Match exato em crm (papel EXAME). |
medico_laudo_crm | string | não | Resolve medico_laudo_id. Match exato em crm (papel LAUDO). |
unidade_nome | string | não | Resolve 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
| HTTP | error.code | Quando ocorre |
|---|---|---|
201 | — | Created. Headers: Location: /v1/exames/{id}, Idempotency-Replay: false. |
400 | VAL_INVALID_PARAM | Idempotency-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. |
400 | VAL_FILE_TOO_LARGE | Arquivo inline acima de 20 MB. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem exames:write. |
409 | CONCURRENT_REQUEST | Mesma Idempotency-Key em andamento. Retry-After: 2. |
422 | IDEMPOTENCY_CONFLICT | Mesma key + body diferente. |
422 | PARAM_CONFLICT | *_id e o alias da mesma FK apontam para registros diferentes (ver resolução). |
422 | RESOLUTION_NOT_FOUND | Alias enviado não casou nenhum registro exato. |
422 | RESOLUTION_AMBIGUOUS | Alias casou 2+ registros (resposta traz candidatos[], máx. 10). |
422 | VALIDATION_ERROR | Validação do backend (ex.: médico laudante obrigatório não informado). |
422 | INVALID_REFERENCE | Paciente/médico/procedimento/convênio inexistente. |
422 | REPORT_ALREADY_EXISTS | Exame já possui laudo (imutabilidade). |
503 | SERVICE_UNAVAILABLE | Falha de comunicação com o backend; resultado cacheado sob a mesma key. |
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 resolvida | Fonte (backend search) | Match exato em |
|---|---|---|---|
paciente_cpf | paciente_id | pacientes/search | cnpj_cpf (11 díg.) |
procedimento_codigo_tuss | procedimento_id | procedimentos/search | codigo_tuss |
procedimento_abreviacao | procedimento_id | procedimentos/search | abreviacao |
convenio_codigo_ans | convenio_id | convenios/search | codigo_ans ¹ |
convenio_nome | convenio_id | convenios/search | nome |
medico_exame_crm | medico_exame_id | medicos/search (EXAME) | crm |
medico_laudo_crm | medico_laudo_id | medicos/search (LAUDO) | crm |
unidade_nome | unidade_id | unidades/search | nome |
¹ 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)
-
O
*_idvence. Se o cliente envia o*_id(> 0) e um alias da mesma FK que resolve para um id diferente →422 PARAM_CONFLICT. Um alias que não resolve (not_found/ambiguous/inválido) ao lado de um id explícito é ignorado (o backend valida noput). -
Sem
*_id, com alias → resolve viasearch:0exatos →422 RESOLUTION_NOT_FOUND;2+exatos →422 RESOLUTION_AMBIGUOUS(comcandidatos[], máx. 10);1exato → injeta o*_id. -
Sem
*_ide 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" } ]
}
}
}
POST
/v1/exames/{id}/laudo
— Ingerir laudo já assinado
Idempotency-Key (UUID v4
recomendado, TTL 24h). Sem ele →
400 VAL_INVALID_PARAM. Mesma key + mesmo body
→ resposta cacheada (201 replay). Ver
Idempotência.
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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
medico_laudo_id | integer | sim | Médico responsável pelo laudo (precisa ter permissão de laudar). |
arquivo.nome | string | sim | Nome do PDF (ex.: laudo-assinado.pdf). |
arquivo.base64 | string | sim | Conteúdo do PDF assinado em base64. Somente PDF. Cap 20 MB. Aceita prefixo data URI. |
titulo | string | não | Título do laudo. Default: referencia ou "Laudo". |
referencia | string | não | Referência/título livre. |
cid_id | integer | não | FK 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
| HTTP | error.code | Quando ocorre |
|---|---|---|
201 | — | Created. Headers: Location: /v1/laudos/{idLaudo}, Idempotency-Replay: false. |
400 | VAL_INVALID_PARAM | Idempotency-Key ausente/inválida, campo obrigatório faltando, arquivo não-PDF, arquivo > 20 MB ou campo bloqueado enviado. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem laudos:write. |
404 | RES_NOT_FOUND | Exame inexistente. |
409 | CONCURRENT_REQUEST | Mesma Idempotency-Key em andamento. Retry-After: 2. |
422 | REPORT_ALREADY_EXISTS | Exame já possui laudo (imutabilidade) — nunca sobrescreve. |
422 | INVALID_REFERENCE | Médico inexistente ou sem permissão para laudar. |
422 | VALIDATION_ERROR | Validação genérica do backend. |
503 | SERVICE_UNAVAILABLE | Falha 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
}
}
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)
| Nome | Tipo | Default | Obrigatório | Descrição |
|---|---|---|---|---|
exame_id |
integer > 0 | — | não | FK laudo.idexamelaudo → exames.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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
400 | VAL_INVALID_PARAM | Param fora da whitelist |
401 | AUTH_* | Falha de autenticação |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem laudos:read |
405 | METHOD_NOT_ALLOWED | Outro método que não GET |
Schema de resposta (200)
| Campo | Tipo | Descrição |
|---|---|---|
data[].id | integer | PK do laudo (laudo.idlaudo) |
data[].exame | objeto ExameRef ou null | {id, paciente, tipo_exame}; null em laudo órfão |
data[].exame.id | integer | FK idexamelaudo |
data[].exame.paciente | objeto {id, nome} | — |
data[].exame.tipo_exame | objeto {id, nome} | — |
data[].medico_laudo | objeto {id, nome} ou null | Médico laudante |
data[].data_laudo | YYYY-MM-DD | Data do laudo |
data[].hora_laudo | HH:MM:SS ou null | — |
data[].assinado | boolean | Mapeia laudo.assinado_digitalmente |
data[].assinado_em | ISO 8601 UTC ou null | Timestamp ICP, quando aplicável |
data[].tem_arquivo | boolean | true se PDF do laudo anexado |
data[].criado_em | ISO 8601 UTC | — |
meta.limit/offset/total/has_more | integer/boolean | Paginaçã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
}
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)
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id |
integer > 0 | sim | PK do laudo (laudo.idlaudo). |
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
400 | VAL_INVALID_PARAM | id não é inteiro positivo |
401 | AUTH_* | Falha de autenticação |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem laudos:read |
404 | RES_NOT_FOUND | ID inexistente ou oculto pelo auto-filtro AMPLIADO |
Schema de resposta (200)
| Campo | Tipo | Descrição |
|---|---|---|
data.id | integer | PK |
data.exame | objeto ExameRef ou null | {id, paciente, tipo_exame}; null em laudo órfão |
data.medico_laudo | objeto {id, nome} ou null | — |
data.data_laudo | YYYY-MM-DD | — |
data.hora_laudo | HH:MM:SS ou null | — |
data.assinado | boolean | — |
data.assinado_em | ISO 8601 UTC ou null | Preenchido apenas se assinado=true |
data.assinatura | objeto {tipo} ou null | Apenas tipo (ex.: "ICP-BRASIL"); null quando não assinado |
data.tem_arquivo | boolean | — |
data.url_arquivo | string (URL relativa) ou null | Ex.: /v1/laudos/516/arquivo |
data.criado_em | ISO 8601 UTC | — |
data.atualizado_em | ISO 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
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
}
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)
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
id |
integer > 0 | sim | PK do laudo. |
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
302 | — | Redirect para presigned S3 |
401 | AUTH_* | Falha de autenticação |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem laudos:read |
404 | RES_NOT_FOUND | Laudo inexistente ou oculto pelo auto-filtro AMPLIADO |
404 | RES_ARQUIVO_NAO_DISPONIVEL | Laudo órfão ou PDF ainda não gerado |
500 | RES_ARQUIVO_S3_FALHA | Falha temporária ao gerar presigned |
Schema de resposta (302)
| Campo | Tipo | Descrição |
|---|---|---|
Header Location | string (URL S3) | Presigned com X-Amz-Expires=300 |
| Body | vazio | — |
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
idexamelaudová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
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)
| Nome | Tipo | Default | Obrigatório | Descriçã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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK (lista paginada) |
400 | VAL_INVALID_PARAM | paciente_id ausente, status fora da whitelist, datas inválidas, limit > 200. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem agenda:read. |
405 | METHOD_NOT_ALLOWED | Verbo não suportado. |
Schema de resposta (200)
| Campo | Tipo | Descrição |
|---|---|---|
data[].id | integer | ID do agendamento. |
data[].paciente | object | { id, nome } — sem PII adicional. |
data[].medico | object | { id, nome }. |
data[].sala | object | null | { id, nome } quando aplicável. |
data[].unidade | object | { id, nome }. |
data[].procedimento | object | { id, nome }. |
data[].agenda_config_id | integer | FK agenda_config.id. |
data[].data_agendamento | YYYY-MM-DD | Data do agendamento. |
data[].hora_inicio | HH:MM | Hora de início. |
data[].hora_fim | HH:MM | Hora de fim — inferida pelo backend na criação. |
data[].status | string | Um dos 8 valores da whitelist. |
data[].encaixe | boolean | true quando o agendamento é encaixe fora da grade. |
data[].cancelado_em | ISO 8601 | null | Carimbo do cancelamento, se aplicável. |
data[].criado_em | ISO 8601 | Carimbo de criação. |
meta | object | { 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 emagenda_agendamentosnão são expostos pela API pública. UseGET /v1/pacientes/{id}com escopopacientes:readseparado.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 cujaagenda_config.medico_id = msão visíveis. O parâmetromedico_idda 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
}
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
| Nome | Tipo | Descrição |
|---|---|---|
id | inteiro > 0 | ID 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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
400 | VAL_INVALID_PARAM | ID inválido (não inteiro positivo). |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem agenda:read. |
404 | RES_NOT_FOUND | ID inexistente OU auto-filtro bloqueia. |
Schema do histórico
Array data.historico ordenado cronologicamente
(mais antigo primeiro):
| Campo | Tipo | Descrição |
|---|---|---|
status_anterior | string | Status antes da transição (whitelist). |
status_novo | string | Status após a transição (whitelist). |
data_hora | ISO 8601 UTC | Carimbo da transição. |
observacao | string | null | Texto 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
}
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)
| Nome | Tipo | Default | Obrigatório | Descriçã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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
400 | VAL_INVALID_PARAM | procedimento_id ausente, horizonte_dias > 180. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem agenda:read. |
405 | METHOD_NOT_ALLOWED | Verbo 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.
| Campo | Tipo | Descrição |
|---|---|---|
data[].medico_id | integer | 0 quando o vínculo é por sala. |
data[].sala_id | integer | 0 quando o vínculo é por médico. |
data[].is_medico | boolean | true = grupo por médico; false = por sala. |
data[].label | string | Nome de exibição (montado a partir de saudacao + nome). |
data[].unidade_id | integer | FK da unidade. |
data[].proximo_slot | object | 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
}
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)
| Nome | Tipo | Default | Descrição |
|---|---|---|---|
medico_id | inteiro > 0 | — | Ignorado se a chave tem medico_id. |
sala_id | inteiro > 0 | — | Filtra apenas configs com tipo_vinculo=sala. |
unidade_id | inteiro > 0 | — | Filtra agenda_config.unidade_id. |
tipo_vinculo | medico | sala | — | Whitelist estrita. |
ativo | 0 | 1 | — | Whitelist estrita. Outro valor → 400. |
nome | string (≥ 3) | — | ILIKE %nome%, mínimo 3 chars após trim. |
limit | integer (1–200) | 50 | Teto 200. |
offset | integer (≥ 0) | 0 | Paginação. |
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK |
400 | VAL_INVALID_PARAM | Whitelist violada (ativo, tipo_vinculo), limit > 200. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem agenda:read. |
405 | METHOD_NOT_ALLOWED | Verbo 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
}
POST
/v1/agenda/agendamentos
— Criar agendamento
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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
agenda_config_id | integer | sim | FK agenda_config.id. |
procedimento_id | integer | sim | FK exame.id. |
convenio_id | integer | sim | 0 = particular. |
data | YYYY-MM-DD | sim | Data do agendamento. |
hora_inicio | HH:MM | sim | hora_fim NÃO é enviada — backend infere automaticamente a partir da duração do procedimento. |
paciente_id | integer | condicional | Quando ausente, paciente_nome + paciente_celular obrigatórios. paciente_id vence sobre o snapshot quando ambos vierem. |
paciente_nome | string (1–200) | condicional | Obrigatório quando paciente_id ausente. |
paciente_celular | string | condicional | Obrigatório quando paciente_id ausente. |
paciente_cpf | string (11 dígitos) | não | Snapshot — gravado em agenda_agendamentos mas nunca exposto em GET. |
paciente_nascimento | YYYY-MM-DD | não | Snapshot. |
paciente_sexo | M | F | O | não | Snapshot. Whitelist estrita. |
paciente_email | string | não | Snapshot. |
plano_id | integer | não | Plano do convênio. |
encaixe | boolean | não | Default false. true = agendamento fora da grade. |
observacoes | string | não | Máx. mb_strlen ≤ 500. |
unidade_id | integer | não | Quando 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
| HTTP | error.code | Quando ocorre |
|---|---|---|
201 | — | Created. Headers: Location: /v1/agenda/agendamentos/{id}, Idempotency-Replay: false. |
400 | VAL_INVALID_PARAM | Idempotency-Key ausente / inválida, campos obrigatórios faltando, formato inválido, force=true, ignorar_conflito=true. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem agenda:write. |
404 | RES_NOT_FOUND | Auto-filtro de médico bloqueia agenda alheia. |
409 | CONCURRENT_REQUEST | Mesma Idempotency-Key em flight. Retry-After: 2. |
422 | IDEMPOTENCY_CONFLICT | Mesma key + body diferente. |
422 | CONFLICT | Horário ocupado (outro agendamento no mesmo médico/data/hora). |
422 | DUPLICATE | Paciente já agendado no mesmo dia/hora. |
422 | BUSINESS_RULE | Convê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
}
POST
/v1/agenda/agendamentos/{id}/cancelar
— Cancelar agendamento
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
| Nome | Tipo | Descrição |
|---|---|---|
id | inteiro > 0 | ID do agendamento. |
Body (JSON)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
motivo | string | sim | mb_strlen 3–500. Fora do range → 400 VAL_INVALID_PARAM. |
force | — | — | BLOQUEADO. 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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK (idempotent_replay=false na 1ª vez). |
400 | VAL_INVALID_PARAM | Idempotency-Key ausente/inválida, motivo curto/longo/ausente, force=true. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem agenda:write. |
404 | RES_NOT_FOUND | ID inexistente OU auto-filtro de médico bloqueia. |
409 | CONCURRENT_REQUEST | Mesma key em flight. Retry-After: 2. |
422 | INVALID_STATE | Agendamento já cancelado ou finalizado. |
422 | BUSINESS_RULE | Sem permissão de unidade (modo única). |
Replay do cancelar
- Cancelar a mesma id duas vezes com
Idempotency-Keydiferente: 1ª devolve200, 2ª devolve422 INVALID_STATE. - Com
Idempotency-Keyigual: 2ª devolve200replay (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
}
POST
/v1/agenda/agendamentos/{id}/reagendar
— Reagendar (id preservado)
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
| Nome | Tipo | Descrição |
|---|---|---|
id | inteiro > 0 | ID do agendamento (PRESERVADO na resposta). |
Body (JSON)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
nova_data | YYYY-MM-DD | sim | Nova data. |
nova_hora_inicio | HH:MM | sim | Nova hora de início. hora_fim é inferida pelo backend. |
agenda_config_id | integer | não | Quando ausente, mantém a agenda atual. |
motivo | string | não | Quando presente, ocupa reagendado_motivo. |
force | — | — | BLOQUEADO. |
ignorar_conflito | — | — | BLOQUEADO. |
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
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | OK (id preservado). |
400 | VAL_INVALID_PARAM | Datas/horário inválidos, Idempotency-Key ausente, force / ignorar_conflito. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem agenda:write. |
404 | RES_NOT_FOUND | ID inexistente OU auto-filtro de médico bloqueia — em ambos os lados. |
409 | CONCURRENT_REQUEST | Mesma key em flight. |
422 | CONFLICT | Novo horário ocupado. |
422 | INVALID_STATE | Cancelado/finalizado não pode ser reagendado. |
422 | BUSINESS_RULE | Procedimento 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
}
POST
/v1/pacientes/{id}/prontuarios
— Criar prontuário (mensagem e/ou anexos)
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.
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)
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
mensagem | string (HTML) | condicional | Texto/HTML do prontuário. Conta como mensagem com mais de 3 caracteres. Obrigatório se não houver anexos. |
anexos | array | condicional | Lista de anexos. Obrigatório se não houver mensagem. |
anexos[].nome | string | sim | Nome do arquivo (ex.: exame.pdf). A extensão define o tipo. |
anexos[].base64 | string | sim | Conteúdo em base64. PDF, PNG ou JPG. Cap 20 MB. Aceita prefixo data URI. |
anexos[].descricao | string | não | Legenda do anexo. Default: o nome. |
exibir | integer (0|1) | não | Visí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
| HTTP | error.code | Quando ocorre |
|---|---|---|
201 | — | Created. Headers: Location: /v1/pacientes/{id}/prontuarios, Idempotency-Replay: false. |
400 | VAL_INVALID_PARAM | Idempotency-Key ausente/inválida, nem mensagem nem anexo, anexo inválido/> 20 MB, extensão não permitida ou campo bloqueado. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem prontuarios:write. |
404 | RES_NOT_FOUND | Paciente inexistente. |
409 | CONCURRENT_REQUEST | Mesma Idempotency-Key em andamento. Retry-After: 2. |
422 | IDEMPOTENCY_CONFLICT | Mesma key com body diferente. |
422 | VALIDATION_ERROR | Validação do backend (anexo/arquivo inválido). |
503 | SERVICE_UNAVAILABLE | Falha 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
}
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âmetro | Tipo | Descrição |
|---|---|---|
limit | integer | Default 50, máx. 200. |
offset | integer | Deslocamento de paginação. Default 0. |
Campos retornados (por item)
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID do prontuário. |
data_emissao | string | Data/hora de emissão. |
mensagem | string (HTML) | Conteúdo do prontuário. |
exibir | boolean | Visível ao paciente. |
rascunho | boolean | Rascunho (via API sempre false). |
medico | object|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
}
GET
/v1/pacientes/{id}/prontuarios/anexos
— Metadados dos anexos
.../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)
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID do anexo (use no endpoint de URL). |
descricao | string | Legenda do anexo. |
extensao | string | Ex.: PDF, PNG, JPG. |
tamanho | integer | Tamanho 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
}
GET
/v1/pacientes/{id}/prontuarios/anexos/{anexoId}/url
— URL do anexo (302 presigned)
{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
| HTTP | error.code | Quando ocorre |
|---|---|---|
302 | — | Redirect para a presigned S3. Location + Cache-Control: no-store. |
400 | VAL_INVALID_PARAM | id/anexoId não inteiro positivo. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem prontuarios:read. |
404 | RES_NOT_FOUND | Anexo inexistente ou de outro paciente (anti-IDOR). |
503 | SERVICE_UNAVAILABLE | Backend 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
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)
| Param | Tipo | Descrição |
|---|---|---|
q | string | Prefixo (LIKE) no nome. Mínimo 2 chars. |
ativo | integer (0/1) | Default 1 (somente ativos). |
limit / offset | integer | Paginação (limit 1–200, default 50). |
Campos retornados (por item)
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID do procedimento (use em procedimento_id). |
nome | string | Nome do procedimento. |
abreviacao | string | Sigla curta. |
codigo_tuss | string | Código TUSS (vazio se não cadastrado). |
descricao_tuss | string | Descrição TUSS. |
requer_laudo | boolean | true = procedimento gera laudo médico. |
possui_imagens | boolean | Procedimento de imagem. |
ativo | boolean | Ativo 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
}
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)
| Param | Tipo | Descrição |
|---|---|---|
q | string | Prefixo (LIKE) no nome do convênio. Mínimo 2 chars. |
ativo | integer (0/1) | Aceito por simetria (schema legado não faz soft-delete de convênio). |
limit / offset | integer | Paginação. |
Campos retornados (por item)
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID do convênio. |
nome | string | Nome do convênio. |
abreviacao | string | Sigla curta. |
codigo_ans | string | Vazio na v1 (schema legado não armazena). |
ativo | boolean | Ativo no tenant. |
planos | array | Sempre 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
}
GET
/v1/medicos
— Médicos (filtro por papel)
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)
| Param | Tipo | Descrição |
|---|---|---|
papel | string (multi) | EXAME · LAUDO · SOLICITANTE. Multi-valor = união (OR): ?papel=EXAME&papel=LAUDO. |
q | string | Prefixo (LIKE) no nome. Mínimo 3 chars. |
ativo | integer (0/1) | Default 1. |
limit / offset | integer | Paginação. |
Campos retornados (por item)
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID do médico. |
nome | string | Nome do médico. |
saudacao | string | Tratamento (ex.: DRA). |
conselho | objeto | { sigla, numero, uf } — ex.: CRM 84122 MG. |
especialidade | string | Especialidade principal. |
papeis | array | Papéis ativos (ex.: ["EXAME","LAUDO"]). |
ativo | boolean | Ativo 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
}
GET
/v1/medicos/{id}
— Detalhe do médico
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
}
POST
/v1/medicos
— Criar médico
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
| Campo | Obrig. | Descrição |
|---|---|---|
nome | sim | 3–100 caracteres. |
saudacao / saudacao_id | sim | Texto (ex.: Dr.) ou id. Ambíguo/inexistente → 422 com sugestoes/candidatos. |
grupo_permissao / grupo_permissao_id | sim | Texto (ex.: MEDICO) ou id. |
papeis | sim | ≥1 de EXAME · LAUDO · SOLICITANTE · SOLICITANTE_EXTERNO. |
usuario | sim | Login de acesso (≥3; A-Z a-z 0-9 . _ - @). |
senha | sim | Write-only (≥3), nunca retornada. |
conselho | não | { tipo, numero } (ex.: CRM 123456). |
cpf | não | CPF (11) ou CNPJ (14) dígitos. |
rg, data_nascimento, sexo | não | sexo = MAS · FEM · OUT. |
telefone, celular, email | não | Contatos. |
endereco | não | { cep, logradouro, numero, complemento, bairro, cidade, estado }. |
cor_agenda, ativo | não | cor_agenda = #rrggbb. |
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
}
PATCH
/v1/medicos/{id}
— Atualizar médico (parcial)
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/email →
422. 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"
}
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)
| Param | Tipo | Descrição |
|---|---|---|
q (obrigatório) | string | Match em código (LIKE 'q%') OU descrição (LIKE '%q%'). Mínimo 2 chars. |
limit | integer | 1–100, default 50 (cap mais restrito). |
offset | integer | Paginação. |
Campos retornados (por item)
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID do CID. |
codigo | string | Código CID (ex.: A09, K59.0). |
descricao | string | Descrição do CID. |
ativo | boolean | Ativo. |
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | Resultado da busca (lista, pode ser vazia). |
400 | VAL_INVALID_PARAM | q ausente ou com menos de 2 chars. |
403 | AUTH_INSUFFICIENT_SCOPE | Chave 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
}
GET
/v1/unidades
— Catálogo de unidades
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)
| Param | Tipo | Descrição |
|---|---|---|
limit | integer | 1–200, default 50. |
offset | integer | Paginação. Sem filtro q (tenants têm poucas unidades). |
Campos retornados (por item)
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID da unidade. |
nome | string | Nome da unidade (use como unidade_nome). |
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | Lista de unidades (pode ser vazia). |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem catalogos:read. |
405 | METHOD_NOT_ALLOWED | Mé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
}
GET
/v1/complementos
— Catálogo de complementos
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)
| Param | Tipo | Descrição |
|---|---|---|
limit | integer | 1–200, default 50. |
offset | integer | Paginação. Sem filtro q (tenants têm poucos complementos). |
Campos retornados (por item)
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID do complemento (use como complementos_id). |
nome | string | Nome do complemento. |
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | Lista de complementos (pode ser vazia). |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem catalogos:read. |
405 | METHOD_NOT_ALLOWED | Mé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
}
GET
/v1/grupos-laudos
— Catálogo de grupos de laudo
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)
| Param | Tipo | Descrição |
|---|---|---|
limit | integer | 1–200, default 50. |
offset | integer | Paginação. Sem filtro q (tenants têm poucos grupos). |
Campos retornados (por item)
| Campo | Tipo | Descrição |
|---|---|---|
id | integer | ID do grupo (use como grupos_laudos_id). |
nome | string | Nome do grupo de laudo. |
Códigos de resposta possíveis
| HTTP | error.code | Quando ocorre |
|---|---|---|
200 | — | Lista de grupos de laudo (pode ser vazia). |
403 | AUTH_INSUFFICIENT_SCOPE | Chave sem catalogos:read. |
405 | METHOD_NOT_ALLOWED | Mé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
}