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. 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. 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. 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. 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. 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. 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. 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. 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. 9

    Atenda os direitos do titular

    POST /erasure cobre portabilidade e eliminação; a revogação usa POST /consents/refusal.

  10. 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çalhoConteúdo
x-vital-timestampEpoch em segundos. Tolerância de 5 minutos.
x-vital-signatureHMAC-SHA256 de timestamp.corpo em hexadecimal minúsculo.
content-typeapplication/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_url e 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_pending e 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-code confirma 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 aceitosPDF, JPEG, PNG, WEBP, HEIC e texto puro
Tamanho máximo por arquivo100 MB no caminho direto; 25 MB por chamada no proxy com Bearer
Download por media.urlaté 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 arquivo413 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 assinatura5 minutos
Idempotênciaidempotency_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/startaté 50 (expected_files)
Termo de consentimentoversionado no Vital; consulte sempre GET /consents/current
Documentos por chamada de /processaté 3
Cadência recomendada de /process1 chamada por minuto
Validade do magic link15 minutos, uso único
Validade da senha provisória7 dias; trocada obrigatoriamente no primeiro acesso
Validade do código de verificação10 minutos, uso único
Emissão de acesso ou código1 pedido por minuto por titular
Tentativas por código5; depois é preciso pedir um novo
Carência da eliminação de dados7 dias, cancelável
Pessoas por contasem limite; só o titular acessa o painel
Escolha da pessoa no envioobrigatória (member_id) quando a conta tem mais de uma pessoa
Formato do telefoneE.164; número brasileiro sem o 55 é normalizado automaticamente. Número impossível responde 400 invalid_phone
Consulta de telefone sem cadastroresponde 200 com exists=false (nunca 404); a decisão é pelo campo exists
Formato do CPFtexto 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ódigoHTTPSignificado
missing_signature401Faltou `x-vital-signature` ou `x-vital-timestamp`.
invalid_signature401A assinatura não corresponde ao corpo enviado.
stale_timestamp401Timestamp fora da janela de 5 minutos.
invalid_payload400O corpo não passou na validação do schema.
invalid_phone400Telefone 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_cpf400CPF informado não é válido. Envie sempre como texto de 11 dígitos.
invalid_birth_date400Data de nascimento no futuro, fora de faixa plausível ou inválida.
email_in_use400E-mail já vinculado a outro titular.
rate_limited429Nova emissão de acesso ou de código pedida antes de 1 minuto.
not_found400Titular sem conta de acesso utilizável para a operação pedida.
missing_media400Nenhum de `url`, `base64` ou `text` foi informado.
empty_media400O arquivo chegou vazio.
consent_required403Sem aceite ativo do termo para este titular.
patient_not_found404Nenhum 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_id400document_id não é um UUID válido.
document_not_found404Nenhum envio com esse document_id.
media_too_large413Arquivo acima de 100 MB no caminho direto, ou acima de 25 MB no proxy com Bearer (`reason: too_large`, com `limit_bytes`).
unsupported_media_type415Formato de arquivo não aceito (`reason: unsupported_mime`).
media_unreachable422O 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.