Montar a UI
Como transformar o payload do relatório em tela: quais campos governam cada card, o que exibir em cada estado e o que nunca deve aparecer.
O Shape do Payload diz o que cada campo é. Esta página diz o que desenhar com ele: para cada campo, o relatório renderizado ao vivo, as variações que aquele campo pode assumir, a resposta JSON correspondente e um prompt pronto para colar na sua ferramenta de IA.
O relatório abaixo é o nosso, o mesmo que o cliente final vê. Os dados são de exemplo, mas passam
pelo mesmo cálculo de diagnóstico da produção, e o JSON ao lado sai da mesma função que responde
GET /api/v1/reports/{reportId} — então nada aqui pode divergir do que a sua integração recebe.
Troque as variações e observe o que muda.
O modelo mental
A tela tem quatro camadas, de cima para baixo — e esta página segue exatamente essa ordem, para que você possa construir na sequência em que o leitor lê:
- O veredito —
diagnosis.statusresume tudo em uma palavra, com os contadores de alerta ao lado. - O medidor de pontos — o número que decide se há processo de suspensão em curso.
- Os quatro indicadores — validade, categoria, exames e bloqueios, cada um com o seu próprio tom.
- A análise em texto e os detalhes — a leitura em linguagem natural, e depois cada seção aberta por cartão.
Três regras valem antes de qualquer pixel:
Só desenhe com o relatório pronto. status === "DELIVERED" e diagnosis !== null. É uma
escolha conservadora de renderização, não uma afirmação sobre o payload: PROCESSING nem sempre vem
vazio — ver estados de carregamento e erro.
Chave ausente não é null. Dentro de diagnosis, as seções points, fines, rollingPoints e
autosuspensionRisk não têm chave quando não há dado — testar === null nunca acusa nada. No
espelho normalized, a chave existe e vem null. Escolha um dos dois acessos e seja consistente.
Vazio não é zero. Um medidor em 0/20 desenhado sobre ausência de dado afirma algo falso.
Renderize o estado vazio quando não houver dado nenhum a mostrar: nem relatório completo, nem parcial, e o veredito não sendo "sem CNH" (que é uma resposta, não uma ausência).
De/para em uma tabela
Na ordem em que aparecem na tela.
| Onde | Componente | Campos que o alimentam |
|---|---|---|
| Painel | Cabeçalho do veredito | diagnosis.status, driver.name |
| Painel | Contadores de alerta | diagnosis.allAlerts[].severity |
| Painel | Medidor de pontos | diagnosis.points.*, diagnosis.fines.totalFines |
| Painel | Indicador de validade | diagnosis.validity.* |
| Painel | Indicador de categoria | diagnosis.category.* |
| Painel | Indicador de exames | diagnosis.medicalExams.*, diagnosis.toxicological.* |
| Painel | Indicador de bloqueios | diagnosis.blocks.* |
| Análise | Texto em linguagem natural | aiAnalysis.markdown |
| Detalhes | Cartão de pontuação | diagnosis.points.*, diagnosis.fines.fines[] |
| Detalhes | Linha do tempo da validade | diagnosis.validity.expiryDate |
| Detalhes | Calendário do toxicológico | diagnosis.toxicological.suggestedExamDates |
| Detalhes | Lista de bloqueios e suspensões | diagnosis.blocks.* |
| Detalhes | Alertas de risco | diagnosis.rollingPoints, diagnosis.autosuspensionRisk |
| Detalhes | Identificação no envelope | driver.* |
| Detalhes | Dados pessoais do titular | details.personal.* |
| Detalhes | Habilitação completa | details.license.* |
| Detalhes | Cursos e reciclagens | details.courses.* |
| Detalhes | Histórico completo de multas | details.stateLookup.fines[] |
Construindo com IA
Há dois prompts, e eles servem a momentos diferentes:
- O prompt de instruções, no fim do explorador acima, carrega o contrato inteiro: fluxo, seções, valores possíveis, mapa de cores e um exemplo completo de resposta. Use-o para construir a interface do zero.
- O prompt de cada componente, na aba Prompt para IA de qualquer demonstração desta página, é autocontido: repete o contrato mínimo e traz o recorte real daquele campo. Use-o para acrescentar uma peça a uma interface que já existe.
Os dois são agnósticos de stack — descrevem dados, estados e tons semânticos, e deixam o mapeamento para os tokens do seu design system.
Veredito
A conclusão em uma palavra. Vence sempre a condição mais severa:
BLOCKED > EXPIRED > WARNING > VALID.
| Valor | Tom | Rótulo que usamos |
|---|---|---|
VALID | sucesso | Regular |
WARNING | atenção | Atenção |
BLOCKED | perigo | Bloqueado |
EXPIRED | perigo | Bloqueado |
UNKNOWN | neutro | Pendente |
NO_LICENSE | neutro | Sem CNH |
BLOCKED e EXPIRED compartilham tom e mensagem de propósito: nos dois casos a pessoa não pode
dirigir. Mantenha ícones distintos, porque a causa e a solução são diferentes. NO_LICENSE é
neutro, não é falha — "não há CNH" é o resultado da consulta.
Dois estados sem demonstração ao vivo
UNKNOWN e hasPartialData: true não têm variação nos demos desta página: os dois dependem de uma
consulta que não respondeu, e o demo é construído a partir de respostas completas. Trate-os pela
regra — UNKNOWN é estado vazio, hasPartialData mostra o que veio e sinaliza o que falta — e não
por semelhança com algo que você viu aqui.
Alertas
allAlerts é a lista achatada de tudo o que as seções sinalizaram.
type é string, não um enum fechado: os valores em snake_case vêm das seções (cnh_expired,
cnh_expiring_soon, medical_exam_expired, toxicological_exam_required,
toxicological_exam_expired, federal_block, unresolved_block, state_suspension,
state_revocation, fines_during_suspension, rolling_limit_exceeded,
autosuspensive_no_trigger, no_reports_available, partial_data_available, no_license_found) e
os em CAIXA ALTA vêm da pontuação (CRITICAL_POINTS, HIGH_POINTS, GRAVISSIMAS_DETECTED). A
lista pode crescer sem que isso seja mudança breaking — trate o desconhecido com um default.
Para os contadores do cabeçalho: crítico conta critical e error; atenção conta warning.
Há dado para diagnosticar
hasReportData e hasPartialData são o portão do estado vazio. Com os dois falsos e o veredito
diferente de NO_LICENSE, não há o que desenhar — mostre um estado vazio, não zeros.
hasPartialData verdadeiro significa que parte das consultas respondeu. Mostre o que veio e sinalize
o que falta, em vez de apresentar um diagnóstico incompleto como se fosse completo.
Pontuação
A seção onde mais se erra.
O limite não é 20
Pela Lei 14.071/2020 o limite é 40 pontos (nenhuma infração gravíssima nos últimos 12 meses, ou
condutor EAR), 30 (uma gravíssima) ou 20 (duas ou mais). O campo limit já chega calculado.
Fixar 20 no código acusa "acima do limite" em condutor regular.
currentPoints é a soma da janela móvel de 12 meses, não o total histórico. E percentage satura
em 100: 45/20 e 60/20 desenham a mesma barra cheia, então a legenda com os números crus é
obrigatória, não decorativa.
riskLevel | Tom | Rótulo |
|---|---|---|
low | sucesso | Baixo Risco |
medium | atenção | Risco Moderado |
high | atenção | Alto Risco |
critical | perigo | Risco Crítico |
Leia limit e riskLevel do payload; não os recalcule. As funções ao lado existem para explicar
por que os valores variam — úteis para escrever a legenda, não para substituir o campo.
Multas
severityBreakdown tem exatamente quatro chaves e alimenta o resumo; fines[] alimenta o detalhe.
Não reconte a partir da lista.
severity | Tom | Rótulo |
|---|---|---|
light | sucesso | Leve |
medium | atenção | Média |
serious | atenção | Grave |
very_serious | perigo | Gravíssima |
value é o valor em reais e pode ser nulo — formate como BRL só quando existir.
Dois campos que não existem em `fines[]`
Um guia de integração anterior mostrava ctbArticle e autosuspensive dentro de fines[]. Nenhum
dos dois existe no contrato. O artigo do CTB e as infrações que suspendem sozinhas vivem em
autosuspensionRisk.
Validade
validityStatus | Tom | Rótulo |
|---|---|---|
valid | sucesso | Válida |
expiring_soon | atenção | A vencer |
expired | perigo | Vencida |
unknown | neutro | Indefinida |
expiring_soon é a janela de 30 dias antes do vencimento. unknown não é "válida": é ausência
de resposta sobre a data — nunca pinte de verde.
Uma linha do tempo útil sai de um único campo, expiryDate, com três marcos: abertura da renovação
(30 dias antes), o vencimento, e o limite legal (30 dias depois). Ordene por data — uma CNH já
vencida reordena o trilho.
Categoria
A categoria é um fato, não um veredito: renderize sem cor de status e sem ponto colorido. Os outros três indicadores comunicam situação; este comunica um dado.
requiresToxicological decorre dela (verdadeiro para C, D e E) e é o que torna a seção toxicológica
obrigatória — com ela verdadeira, um toxicológico ausente ou vencido leva o veredito a BLOCKED.
Exames clínicos
exams é opcional: teste a existência da chave, não o valor. Um exame vencido leva o veredito a
WARNING, não a BLOCKED.
Exame toxicológico
A única seção clínica que bloqueia. Com isRequired verdadeiro, hasExam falso ou isExpired
verdadeiro levam o veredito a BLOCKED.
suggestedExamDates é um calendário futuro, não um histórico: cada item traz date, isPast e
daysFromNow. Marque o próximo e destaque o atrasado.
Bloqueios e suspensões
Aqui está a maior armadilha do payload.
Ausente e `false` são respostas diferentes
blocks.stateDeclaredBlock ausente significa que a consulta estadual não chegou a rodar. Só
isBlocked: false é evidência de que a habilitação está regular no estado. Tratar os dois como
iguais faz a sua interface afirmar algo que o dado não sustenta.
O segundo caso a suportar: isBlocked: true com detailedBlocks: 0. O estado acusa bloqueio sem
detalhar nenhum, então o veredito é BLOCKED com a lista de bloqueios vazia. A tela precisa
aguentar isso sem parecer quebrada.
federalBlocks, suspensions e revocations são arrays opcionais — a chave pode não existir.
finesDuringSuspension agrava o processo e merece destaque separado das multas comuns.
Análise em linguagem natural
aiAnalysis.markdown é um texto em markdown pt-BR explicando o diagnóstico. É best-effort: pode
vir nulo num relatório já entregue, sem que isso seja erro. Nunca bloqueie a tela esperando por ela.
Renderize como markdown, não como texto puro, e sanitize antes de injetar HTML.
Janela móvel de pontos
O campo que revela o risco que o número de hoje esconde: currentPoints pode estar baixo agora
porque multas antigas saíram da janela, mesmo tendo havido um pico acima do limite. everExceeded
merece destaque próprio, separado do medidor atual.
Repare que limitAtMoment varia ao longo da série — o limite muda conforme as gravíssimas entram e
saem da janela de 12 meses.
Risco autossuspensivo
Infrações que suspendem a habilitação sozinhas, independentemente da pontuação.
atRisk verdadeiro com hasActiveBlock falso é o caso urgente: existe infração que gera suspensão
direta e o processo ainda não foi instaurado — é a janela da defesa prévia, e a interface deve
tratá-la como ação imediata, não como aviso.
Identificação do condutor
driver é o bloco de identidade do envelope: id, cpf, name, licenseNumber,
licenseCategory, expiryDate e issueState — os sete campos que acompanham o relatório desde a
criação. O registro completo do titular vive em details: filiação, documento de identidade e
naturalidade em details.personal, cursos em details.courses. Veja
onde está cada coisa.
Num relatório NO_LICENSE, name é o nome que você enviou, não um nome confirmado em base
oficial. Um nome preenchido ao lado de "sem CNH" diz que aquele CPF não tem habilitação emitida; não
diz que a pessoa foi identificada.
O espelho normalized
Os mesmos dados num formato de acesso diferente: aqui a chave sempre existe e a ausência é null.
Ele não espelha tudo — rollingPoints e autosuspensionRisk só existem em diagnosis.
Estados de carregamento e erro
status | O que mostrar |
|---|---|
PROCESSING | Dois casos diferentes atrás do mesmo valor — ver abaixo. |
DELIVERED | O diagnóstico completo. |
ERROR | Falha após todas as tentativas; o crédito foi estornado. |
`PROCESSING` não significa payload vazio
O estado interno de entrega parcial — a consulta federal respondeu, a estadual não — também
reporta status: "PROCESSING", e é o corpo do webhook report.partially_delivered. Nele
diagnosis e details já vêm preenchidos com o que foi apurado.
Descartar o payload com if (status !== "DELIVERED") return; joga fora um relatório que tem dado.
Teste diagnosis !== null / details !== null para saber se há o que ler, e o status para saber
se ainda vem mais.
Datas: duas classes, e confundir custa um dia
| Classe | Campos | Leia em |
|---|---|---|
| Calendário | expiryDate, datas de multa, de exame e de bloqueio | UTC |
| Instante | createdAt, deliveredAt, diagnosisDate, generatedAt | fuso de São Paulo |
Uma data de calendário chega em UTC à meia-noite. Formatá-la no fuso local — e o Brasil é UTC−3 — recua o dia: uma CNH que vence em 1º de janeiro aparece vencendo em 31 de dezembro.
Nunca use formatação que dependa do fuso do ambiente. Passe o fuso explicitamente: UTC para datas de
calendário, America/Sao_Paulo para instantes.
Tons: um mapa só
Não invente um segundo mapa de cores. Cinco dimensões, os mesmos cinco tons.
Veredito, validade, risco de pontos, gravidade de multa e severidade de alerta são as cinco dimensões com cor. Mapeie cada uma para os tokens do seu próprio design system e nunca comunique estado apenas por cor — acompanhe de rótulo ou ícone.
Onde está cada coisa
Desde 2026-08-06 o payload tem paridade de campos com o relatório. O que faltava vive em
details, ao lado de diagnosis:
| O que você quer desenhar | Onde está |
|---|---|
| Veredito, indicadores, pontuação da janela de 12 meses | diagnosis |
| Dados pessoais — filiação, documento de identidade, naturalidade | details.personal |
| Habilitação completa — RENACH, 1ª habilitação, local de emissão, situação, EAR | details.license |
| Exames com categoria permitida, cidade e UF | details.medicalExams |
| Toxicológico com data de uso e situação | details.toxicological |
| Cursos e reciclagens | details.courses |
| Histórico completo de multas, com status de pagamento, e o saldo oficial de pontos | details.stateLookup |
Três coisas que vale saber antes de escolher de onde ler:
detailsacompanha a existência do dado, não ostatus, comodiagnosis— ver estados de carregamento e erro.details.stateLookupénullquando a consulta estadual não rodou — a mesma distinção destateDeclaredBlock: ausência não é "sem multas".- Duas listas de multas, propósitos diferentes.
diagnosis.finesé a janela móvel de 12 meses, que decide a pontuação pela Lei 14.071.details.stateLookup.finesé o histórico inteiro. Não use a segunda para calcular pontos.
O demo acima continua alimentado apenas pelo que a API expõe — a diferença é que agora isso é
tudo. Ver Shape do Payload para os tipos de cada seção de
details, e o Changelog para a entrada dessa mudança.
O que continua fora, e por quê
A garantia anti-leak não mudou: payload bruto do provedor, custo, identificador de requisição, nome
do provedor, URLs internas e contadores de pipeline nunca saem. Também ficam fora o código numérico
de status de cada multa (o status já vem parseado) e a origem interna do cálculo de pontos —
detalhes da nossa extração, não do seu relatório.
Ver também
- Shape do Payload — o contrato completo, campo a campo.
- Consultar Relatório — o endpoint que devolve este payload.
- Erros — o catálogo de erros e o tratamento recomendado.
Consultar Relatório
GET /api/v1/reports/{reportId} — polling do status e resultado. Retorna 404 para IDs de outras organizações, sem vazar informação.
Webhooks
Receba o resultado de um relatório assim que ele fica pronto, sem ficar consultando a API: como assinar, verificar a origem e tratar reentrega.