API v1 · chamadas assinadas
Integre seu agente à plataforma Vital
O Vital recebe exames em qualquer formato — PDF, foto, print ou texto — registra o consentimento do titular, lê e normaliza os resultados e devolve ao paciente um painel com evolução dos marcadores e orientação de qual especialidade procurar. Esta é a documentação pública do contrato de integração.
Fluxo da integração
- 1
Identifique e cadastre o titular
GET /patients?phone=… mostra o que já existe e o que falta; POST /patients grava nome completo, data de nascimento, CPF, sexo biológico e e-mail. Só o telefone é obrigatório: campo ausente, vazio ou nulo é tratado como ainda não informado, então o cadastro pode ser completado aos poucos.
- 2
Apresente o termo vigente
GET /consents/current devolve versão, resumo curto e o link público com o texto integral (/termo/{versao}). Mostre o resumo e o link na abertura da conversa; o texto é gerenciado pelo administrador do Vital, então sempre consulte este endpoint em vez de fixar o texto no agente.
- 3
Registre a decisão do titular
POST /consents/acceptance para o aceite e POST /consents/refusal para a recusa ou revogação (POST /consents com accepted continua válido). Envie evidence.accepted_at com o horário real da mensagem: é ele que fica gravado como momento do aceite. Sem aceite ativo, nenhum arquivo é aceito.
- 4
Entregue o acesso ao painel
Desde a 1.12.0 não é preciso pedir nada: a resposta de POST /patients já traz o bloco access com a senha provisória (válida por 7 dias) e o endereço de entrada, quando o titular ainda não tem senha própria. Envie os dois na conversa. O primeiro acesso é só celular + senha, sem código; o painel pede a troca da senha e leva à home. POST /patients/access continua disponível para reemitir.
- 5
Pergunte de quem é o exame
GET /family-members?phone=… devolve as pessoas da conta já numeradas (option, label, parentesco e idade). Telefone sem conta responde 200 com exists=false e members vazio — decida pelo campo exists, sem tratar como erro. Com mais de uma pessoa, mostre a lista e envie o member_id escolhido junto com o arquivo; sem ele o envio responde 409 member_required com a mesma lista. POST /family-members cadastra um parente pela conversa (apelido, parentesco, data de nascimento e sexo).
- 6
Envie os exames
Abra o lote em POST /lab-results/start. Para vários arquivos de uma vez, use POST /lab-results/upload-batch com o batch_id e até 10 itens: cada item tem sua própria idempotency_key (e member_id opcional), é lido em execução própria e a resposta 207 traz results[] na ordem enviada — reenvie só os itens com ok: false. Limites: 10 itens e 100 MB somados por chamada (25 MB pelo proxy do GPT Maker). Para um arquivo por chamada, POST /lab-results/upload com o batch_id, ou POST /documents para um envio isolado.
- 7
Acompanhe a fila
O envio responde 202 na hora, com o corpo JSON completo, e a leitura do exame continua em segundo plano no servidor — a integração não precisa manter a conexão aberta nem esperar. Se a leitura for interrompida, o Vital retoma sozinho: uma varredura interna roda a cada minuto e prioriza os envios mais recentes ainda não concluídos. POST /process continua disponível como rede de segurança manual: com {"document_id":"…"} relê um envio específico e com {} processa a fila. POST /cron/process, POST /cron/prescriptions, POST /cron/reminders e POST /cron/review-maintenance (manutenção da fila de revisão clínica, nos modos refresh e packets, percorrida por cursor) são internos do Vital (Bearer próprio) e não devem ser chamados por integrações. GET /status?phone=… devolve as contagens do titular e GET /status?document_id=… o estado de um envio. A carta de encaminhamento em PDF é gerada apenas dentro do painel autenticado do paciente (aba Prescrições) e não tem endpoint público.
- 8
Entregue o painel por link
POST /magic-link gera um link de uso único, válido por 15 minutos, para o titular abrir o painel.
- 9
Atenda os direitos do titular
POST /erasure cobre portabilidade e eliminação; a revogação usa POST /consents/refusal.
- 10
Fale das prescrições como sugestão em revisão
Desde a 1.14.0 toda sugestão de exame ou encaminhamento passa por revisão de um médico do Vital antes de valer como documento. Enquanto o pacote não é assinado, diga que está em revisão médica; depois de assinado, o paciente baixa o documento na aba Prescrições do painel. Não existe endpoint público para essa fila nem para o documento assinado, e o agente nunca afirma diagnóstico, tratamento ou medicamento. POST /api/public/docusign/webhook é usado apenas pela DocuSign, com HMAC nos cabeçalhos x-docusign-signature-1 a -5 sobre o corpo bruto, e não devolve dado de paciente.
Autenticação
Toda chamada leva dois cabeçalhos. A assinatura é o HMAC-SHA256, em hexadecimal minúsculo, de timestamp.corpo, calculado com o segredo compartilhado com o parceiro. Em requisições GET, o "corpo" é a query string incluindo o ?. Assine sempre o corpo exatamente como ele vai no fio — serialize uma única vez.
| Cabeçalho | Conteúdo |
|---|---|
| x-vital-timestamp | Epoch em segundos. Tolerância de 5 minutos. |
| x-vital-signature | HMAC-SHA256 de timestamp.corpo em hexadecimal minúsculo. |
| content-type | application/json nos métodos com corpo. |
import crypto from "node:crypto";
const BASE = "https://sua-instancia-vital.example";
const SECRET = process.env.VITAL_INGEST_SECRET; // entregue ao parceiro; nunca versione
async function callVital(path, payload) {
const body = JSON.stringify(payload);
const ts = Math.floor(Date.now() / 1000);
const signature = crypto
.createHmac("sha256", SECRET)
.update(`${ts}.${body}`)
.digest("hex");
const res = await fetch(BASE + path, {
method: "POST",
headers: {
"content-type": "application/json",
"x-vital-timestamp": String(ts),
"x-vital-signature": signature,
},
body,
});
return { status: res.status, data: await res.json() };
}
await callVital("/api/public/v1/consents/acceptance", {
patient: { phone: "+5511999998888", full_name: "Maria Souza" },
term_version: "v1.0",
channel: "whatsapp",
evidence: { wamid: "wamid.EXEMPLO", message_text: "ACEITO", accepted_at: new Date().toISOString() },
});O segredo de assinatura é entregue individualmente a cada parceiro e não aparece em nenhum lugar desta documentação. Mantenha-o apenas no servidor, nunca no navegador, no app ou no repositório.
Proxy para agentes sem HMAC (GPTMaker)
Plataformas conversacionais que não calculam assinatura por requisição usam o proxy assinado. Troque o prefixo /api/public/v1 por /api/public/gpt/v1, mantenha o mesmo corpo e envie apenas o cabeçalho Authorization: Bearer … com o token do proxy. O proxy calcula x-vital-timestamp e x-vital-signature, chama a rota v1 correspondente e devolve status e corpo originais.
curl -X POST "$VITAL_BASE_URL/api/public/gpt/v1/consents/acceptance" \
-H "authorization: Bearer $VITAL_PROXY_TOKEN" \
-H "content-type: application/json" \
-d '{"patient":{"phone":"+5511999998888"},"term_version":"2026-08-1"}'
curl "$VITAL_BASE_URL/api/public/gpt/v1/consents/current?phone=%2B5511999998888" \
-H "authorization: Bearer $VITAL_PROXY_TOKEN"Endpoints liberados: /patients, /patients/access, /consents, /consents/current, /consents/acceptance, /consents/refusal, /lab-results/start, /lab-results/upload, /lab-results/upload-batch, /documents, /process, /status, /auth-code, /magic-link e /erasure. Limites: 120 chamadas por minuto e 25 MB por arquivo. Nos envios (/documents e /lab-results/upload) o arquivo pode ir em bytes crus no corpo, com os metadados na query (?phone=…&mime=…&filename=…&batch_id=…) — recomendado quando a URL de mídia do WhatsApp já pode ter expirado. No caminho direto assinado, esse formato assina {timestamp}. seguido dos bytes do corpo. Erros próprios do proxy: unauthorized (401), endpoint_not_allowed (404), method_not_allowed (405), payload_too_large (413), rate_limited (429) e proxy_not_configured (503).
Acesso do titular: senha provisória no primeiro contato
Ao cadastrar o titular em /patients, a resposta já traz o bloco access quando ele ainda não tem senha própria. Use POST /patients/access apenas para reemitir.
- Senha provisória automática: senha forte e fácil de digitar no celular, devolvida uma única vez, com
login_urle validade de 7 dias. Não é possível consultá-la depois; se o titular perder, emita outra. - Entrada sem código. O painel do paciente aceita celular + senha, sem código de 6 dígitos, no primeiro acesso e nos seguintes. Quem entra com a senha provisória é levado à tela de criar a senha definitiva e depois à home.
- Cadastro nunca falha por causa do acesso. Se a emissão cair no limite de 1 por minuto, a resposta traz
access_pendinge o cadastro segue válido. - Recuperação de senha continua por código de 6 dígitos no WhatsApp (10 minutos, uso único, até 5 tentativas), com
mode: "code"em/patients/access. - Reemissão. Uma nova emissão invalida a anterior imediatamente e respeita o limite de 1 pedido por minuto (
rate_limited). POST /auth-codeconfirma se há código ativo para um número (finalidade, expiração e se já foi entregue). O código em si nunca é devolvido pela API.
Nunca registre a senha provisória em log, transcrição ou banco do agente. Envie a mensagem ao titular e descarte o valor.
Limites e regras
| Formatos aceitos | PDF, JPEG, PNG, WEBP, HEIC e texto puro |
|---|---|
| Tamanho máximo por arquivo | 100 MB no caminho direto; 25 MB por chamada no proxy com Bearer |
| Download por media.url | até 30 segundos por tentativa, com até 3 tentativas e espera crescente em falha de rede; URL do WhatsApp Cloud API precisa de media.access_token (ou token no servidor). Falha responde 422 media_unreachable com o motivo em reason |
| Erros de arquivo | 413 media_too_large com reason=too_large e limit_bytes; 415 unsupported_media_type com reason=unsupported_mime — nunca 502 genérico |
| Janela de tolerância da assinatura | 5 minutos |
| Idempotência | idempotency_key (use o id da mensagem) só bloqueia quando o arquivo é o mesmo: chave repetida no mesmo batch_id com conteúdo idêntico responde 200 duplicated:true com reason: idempotency_key_reused; com arquivo diferente o exame novo é aceito normalmente. Há ainda deduplicação por hash do arquivo (reason: duplicate_content). Se o envio anterior falhou, o reenvio reprocessa (202 com reprocessing: true) |
| Arquivos por lote de /lab-results/start | até 50 (expected_files) |
| Termo de consentimento | versionado no Vital; consulte sempre GET /consents/current |
| Documentos por chamada de /process | até 3 |
| Cadência recomendada de /process | 1 chamada por minuto |
| Validade do magic link | 15 minutos, uso único |
| Validade da senha provisória | 7 dias; trocada obrigatoriamente no primeiro acesso |
| Validade do código de verificação | 10 minutos, uso único |
| Emissão de acesso ou código | 1 pedido por minuto por titular |
| Tentativas por código | 5; depois é preciso pedir um novo |
| Carência da eliminação de dados | 7 dias, cancelável |
| Pessoas por conta | sem limite; só o titular acessa o painel |
| Escolha da pessoa no envio | obrigatória (member_id) quando a conta tem mais de uma pessoa |
| Formato do telefone | E.164; número brasileiro sem o 55 é normalizado automaticamente. Número impossível responde 400 invalid_phone |
| Consulta de telefone sem cadastro | responde 200 com exists=false (nunca 404); a decisão é pelo campo exists |
| Formato do CPF | texto de 11 dígitos; como número JSON o valor perde o zero à esquerda e vira notação científica |
Códigos de erro
Erros vêm sempre como { "error": "codigo" }.
| Código | HTTP | Significado |
|---|---|---|
| missing_signature | 401 | Faltou `x-vital-signature` ou `x-vital-timestamp`. |
| invalid_signature | 401 | A assinatura não corresponde ao corpo enviado. |
| stale_timestamp | 401 | Timestamp fora da janela de 5 minutos. |
| invalid_payload | 400 | O corpo não passou na validação do schema. |
| invalid_phone | 400 | Telefone não normalizável para E.164 (DDD inválido, celular sem o nono dígito ou número curto sem DDI). Telefone válido sem cadastro não é erro: a consulta responde 200 com exists=false. |
| invalid_cpf | 400 | CPF informado não é válido. Envie sempre como texto de 11 dígitos. |
| invalid_birth_date | 400 | Data de nascimento no futuro, fora de faixa plausível ou inválida. |
| email_in_use | 400 | E-mail já vinculado a outro titular. |
| rate_limited | 429 | Nova emissão de acesso ou de código pedida antes de 1 minuto. |
| not_found | 400 | Titular sem conta de acesso utilizável para a operação pedida. |
| missing_media | 400 | Nenhum de `url`, `base64` ou `text` foi informado. |
| empty_media | 400 | O arquivo chegou vazio. |
| consent_required | 403 | Sem aceite ativo do termo para este titular. |
| patient_not_found | 404 | Nenhum titular com esse telefone nos endpoints que exigem cadastro prévio (`/magic-link`, `/documents`, `/erasure`). A consulta `GET /patients` não usa este código: responde 200 com `exists: false`. |
| invalid_document_id | 400 | document_id não é um UUID válido. |
| document_not_found | 404 | Nenhum envio com esse document_id. |
| media_too_large | 413 | Arquivo acima de 100 MB no caminho direto, ou acima de 25 MB no proxy com Bearer (`reason: too_large`, com `limit_bytes`). |
| unsupported_media_type | 415 | Formato de arquivo não aceito (`reason: unsupported_mime`). |
| media_unreachable | 422 | O download da URL falhou depois de até 3 tentativas com espera crescente. O campo `reason` diz o motivo: expired_url, forbidden, not_found, timeout, network_error, viewer_page ou empty. `detail` traz o texto pronto para o agente. |
Conformidade obrigatória do integrador
- Consentimento antes de tudo. Apresente o termo de uso de dados sensíveis de saúde e registre o aceite antes de enviar qualquer arquivo.
- Sem diagnóstico. Nem o Vital nem o agente podem afirmar diagnóstico ou indicar tratamento e medicamento. Toda interpretação devolvida traz a fonte do protocolo aplicado e o aviso de não-diagnóstico — reproduza-os junto ao conteúdo.
- Minimização. Envie apenas o necessário; não armazene o conteúdo clínico no seu lado além do tempo da entrega.
- Direitos do titular. Os comandos MEUS DADOS, APAGAR, CANCELAR e REVOGAR precisam funcionar a qualquer momento na conversa.