Pular para o conteúdo
Veltra Ajuda
Manual da APIPlaybookEntrar

Referência da API

Referência de webhooks e eventos de saída

Abrir o Swagger

Referência de webhooks e eventos de saída

Esta página é para o desenvolvedor que vai receber os avisos do Veltra no sistema da transportadora (TMS), de um embarcador ou de um destinatário. Quem vai só cadastrar o endereço na tela encontra o passo a passo no módulo 5. A lista de rotas de consulta está na referência de leitura.

Esta página é escrita à mão a partir do contrato (contracts/webhooks/), dos ADRs 0013 e 0022 e do código da entrega. Mudanças futuras nos eventos são só aditivas: campo novo entra sem subir versao, então ignore campos que você não conhece.

1. O que é um webhook e por que usar#

Pense em uma encomenda. Você pode ir à portaria de hora em hora perguntar "chegou o meu pacote?" (isso é consultar de tempos em tempos, o "polling"). Ou pode deixar o seu telefone na portaria e pedir "me ligue quando chegar" (isso é um webhook).

No Veltra é igual:

  • Webhook: quando algo acontece (uma baixa, uma ocorrência, um comprovante), o Veltra faz uma chamada HTTP POST para um endereço do seu sistema, com os dados do fato.
  • Consulta: o seu sistema pergunta ao Veltra, pela API de leitura (GET /v1/saida/eventos), o que mudou desde a última vez.

Por que preferir o webhook para ser avisado:

Consultar de tempos em temposWebhook
Você descobre o fato só na próxima rodada (minutos de atraso)Você é avisado em poucos segundos
A maioria das chamadas volta vazia e gasta o seu limite por minutoSó há chamada quando há fato
Você escolhe o intervalo: curto gasta recursos, longo atrasaNão há intervalo para escolher

O webhook tem um custo: o seu endereço precisa estar no ar, ser público, e conferir a assinatura de cada chamada. Por isso a melhor prática é usar os dois: o webhook para a velocidade e a consulta incremental (alteradoDesde) como rede de segurança, para reconciliar qualquer coisa que tenha se perdido. Veja a seção 14.

Duas características do Veltra que valem desde já:

  • O webhook leva identificadores e dados curtos, não arquivos. Para o PDF do comprovante, por exemplo, você recebe o comprovanteId e baixa o arquivo pela API de leitura com a sua chave.
  • A entrega é "pelo menos uma vez": se o Veltra não tiver certeza de que você recebeu, manda de novo. Por isso o seu código precisa aceitar repetição (seção 8).

2. Cadastrar o endereço e obter o segredo#

O cadastro é feito por um administrador da transportadora, no portal, em Integrações > Webhooks (/integracoes/webhooks). Cada cadastro é um assinante: um endereço que recebe um conjunto de eventos.

Prepare o endereço no seu sistema

Tenha um endereço HTTPS público que aceite POST com corpo JSON, por exemplo https://SEU_ENDERECO_AQUI/webhooks/veltra. Ele ainda não precisa verificar a assinatura para ser cadastrado, mas precisa responder 2xx quando for chamado.

Abra Webhooks e clique em Criar webhook

Preencha o formulário:

  • Nome do assinante: de 1 a 120 caracteres, por exemplo "TMS da matriz".
  • Quem recebe: "Sistema da transportadora (TMS)" (papel TMS) ou "Terceiro (embarcador, cliente)" (papel TERCEIRO).
  • URL de destino: o endereço do passo 1. Em Produção precisa começar com https://. Endereços internos são recusados (veja a seção 10).
  • Eventos: marque os tipos que você quer. Os eventos aparecem em grupos; nos grupos com mais de um evento há um botão "Marcar todos". Marque só o que for usar.
  • Enviar posição a cada: aparece só para o papel TMS com o evento posicao.lote. Escolha 5, 10 ou 15 minutos.
  • CNPJ do terceiro: só para o papel TERCEIRO. Informe as raízes (8 caracteres) ou o CNPJ inteiro; o portal guarda só a raiz.
  • E-mail de contato técnico: obrigatório. É para onde o Veltra avisa se o webhook for desativado.

Copie o segredo na hora

Ao salvar, o portal mostra a janela "Guarde o segredo agora". O segredo aparece uma única vez. Use "Copiar segredo", guarde em um cofre de segredos (nunca no código nem em log) e só então marque "Já guardei o segredo" e clique em "Concluir". Se perder, gere outro com "Girar segredo" (seção 9).

Configure o segredo no seu sistema

O segredo tem o formato whsec_ seguido de um texto em Base64 (que representa 32 bytes aleatórios). Guarde-o inteiro, com o prefixo, em uma variável de ambiente ou no cofre. A seção 5 mostra como usá-lo.

Envie um teste

Na página do webhook, clique em Enviar teste (seção 11) e confira se o seu endereço respondeu 2xx.

Regras que o cadastro aplica:

  • Tipos: ao menos um, e só os 8 tipos do catálogo (seção 3).
  • posicao.lote: só o papel TMS pode assinar. O cadastro de um TERCEIRO com esse tipo é recusado.
  • veiculo.km_registrado: idem, só o papel TMS.
  • TERCEIRO: precisa de ao menos uma raiz de CNPJ. O terceiro só recebe os eventos de documentos cujo emitente (o embarcador) tem a raiz do CNPJ dentro do escopo dele; evento de documento de outro embarcador, ou sem documento, nunca chega a ele. Se o terceiro não é o tomador do documento, recebe o evento sem os dados de PDF e de canhoto.
  • Cadência: só vale para TMS com posicao.lote; valores aceitos 5, 10 e 15.
  • Ambiente: um assinante pertence a um ambiente, Teste ou Produção (ambiente no corpo; omitido vale Produção), e esse ambiente não muda depois. Eventos de Teste só chegam a assinantes de Teste, e os de Produção só a assinantes de Produção.
  • Editar: nome, URL, tipos, papel, escopo, cadência e e-mail podem ser mudados depois. Mudar a URL não muda o segredo.

O mesmo cadastro existe na API do portal, para quem prefere automatizar (login de administrador, não a chave de API de integração):

AçãoRota
Listar / criarGET e POST /v1/integracoes/webhooks
Ver / editar / excluirGET, PUT e DELETE /v1/integracoes/webhooks/{id}
Pausar / retomarPOST /v1/integracoes/webhooks/{id}/pausa e .../retomada
Girar o segredoPOST /v1/integracoes/webhooks/{id}/rotacao-segredo
Listar entregasGET /v1/integracoes/webhooks/{id}/entregas
Reenviar uma entregaPOST /v1/integracoes/webhooks/{id}/entregas/{entregaId}/reenvio
Enviar testePOST /v1/integracoes/webhooks/{id}/teste

Exemplo de corpo para criar um assinante TMS:

JSON
{
  "nome": "TMS da matriz",
  "url": "https://SEU_ENDERECO_AQUI/webhooks/veltra",
  "tipos": ["baixa.registrada", "ocorrencia.registrada", "comprovante.emitido"],
  "papel": "TMS",
  "contatoEmail": "integracao@SEU_DOMINIO_AQUI"
}

A resposta (201) traz o assinante e o campo segredo, uma única vez.

3. Lista completa de eventos#

São 8 tipos. Todos chegam no mesmo envelope (próxima seção). Para cada um abaixo você vê quando dispara, quem pode assinar, um exemplo completo de corpo e a tabela de campos de dados.

TipoQuando disparaQuem recebeGerado hoje pelo Veltra
parada.chegadao motorista registra a chegada numa paradaTMS e TERCEIROsim
parada.saidao motorista registra a saída de uma paradaTMS e TERCEIROsim
baixa.registradauma baixa de documento é registrada na paradaTMS e TERCEIROsim
ocorrencia.registradauma ocorrência é registrada para um documentoTMS e TERCEIROsim
comprovante.emitidoo comprovante de um documento é emitidoTMS e TERCEIROsim
evento_cte.estado_alteradomuda o estado do evento 110180 (comprovante de entrega) acompanhado pelo VeltraTMS e TERCEIROsim
posicao.lotea cada 5 minutos, por veículo, um lote de posiçõessó TMSsim, só em Produção
veiculo.km_registradoo motorista informa o km do veículo na saída da base ou na chegadasó TMSsim

O envelope#

Todo evento, por webhook ou pela leitura /v1/saida/eventos, usa o mesmo envelope:

CampoTipoObrigatórioDescrição
ambienteenumsimvalores: TESTE, PRODUCAO
dadosobjetosim
idtexto (uuid)simIdentificador do evento. Estável; não é o id da tentativa HTTP.
ocorridoEmtexto (date-time)sim
tipotextosimmín. tamanho: 1
versaoconstantesimVersão do payload. A primeira versão é 1. Campo novo entra sem subir este número.; valor fixo: 1

Pontos de atenção:

  • id identifica o evento e não muda. Ele não é o identificador da entrega HTTP (esse é o cabeçalho webhook-id, seção 4).
  • ocorridoEm é o momento do fato, em UTC (termina em Z).
  • O corpo nunca traz o identificador da transportadora: o assinante já é conhecido pela entrega.
  • dados muda por tipo e é descrito a seguir.

parada.chegada#

O motorista registrou a chegada numa parada. Dispara uma vez por chegada registrada. Quem recebe: TMS e TERCEIRO.

JSON
{
  "id": "018f6a2e-7c31-7a3e-8c2a-000000000004",
  "tipo": "parada.chegada",
  "versao": 1,
  "ocorridoEm": "2026-10-22T15:00:00Z",
  "ambiente": "PRODUCAO",
  "dados": {
    "paradaId": "22222222-2222-4222-8222-222222222222",
    "manifestoId": "11111111-1111-4111-8111-111111111111"
  }
}
CampoTipoObrigatórioDescrição
manifestoIdtexto (uuid)sim
paradaIdtexto (uuid)sim

parada.saida#

O motorista registrou a saída de uma parada. Quem recebe: TMS e TERCEIRO.

JSON
{
  "id": "018f6a2e-7c31-7a3e-8c2a-000000000005",
  "tipo": "parada.saida",
  "versao": 1,
  "ocorridoEm": "2026-10-22T15:20:00Z",
  "ambiente": "PRODUCAO",
  "dados": {
    "paradaId": "22222222-2222-4222-8222-222222222222",
    "manifestoId": "11111111-1111-4111-8111-111111111111"
  }
}
CampoTipoObrigatórioDescrição
manifestoIdtexto (uuid)sim
paradaIdtexto (uuid)sim

baixa.registrada#

Baixa de um documento na parada. Se uma mesma baixa cobrir vários documentos, o Veltra grava um só evento, com o documentoId do primeiro; confira os demais pela consulta. O evento traz o documentoId e o paradaId, mais o mínimo da baixa: capturadoEm, recebidoEm, recebedor.nome, coordenada e quantidadeDeFotos. O documento do recebedor nunca vai no evento (LGPD). Terceiro que não é o tomador não recebe recebedor nem coordenada. Os campos opcionais pdf e canhoto do contrato não são preenchidos hoje. Para o PDF, espere o evento comprovante.emitido e baixe pela API. Terceiro que não é o tomador do documento nunca recebe pdf nem canhoto. Quem recebe: TMS e TERCEIRO.

JSON
{
  "id": "018f6a2e-7c31-7a3e-8c2a-000000000006",
  "tipo": "baixa.registrada",
  "versao": 1,
  "ocorridoEm": "2026-10-22T15:10:00Z",
  "ambiente": "PRODUCAO",
  "dados": {
    "baixaId": "44444444-4444-4444-8444-444444444444",
    "documentoId": "33333333-3333-4333-8333-333333333333",
    "paradaId": "22222222-2222-4222-8222-222222222222",
    "capturadoEm": "2026-10-22T15:10:00Z",
    "recebidoEm": "2026-10-22T15:10:07Z",
    "recebedor": { "nome": "Maria da Silva" },
    "coordenada": { "latitude": -23.5505, "longitude": -46.6333, "precisaoM": 12.5 },
    "quantidadeDeFotos": 2
  }
}
CampoTipoObrigatórioDescrição
baixaIdtexto (uuid)sim
capturadoEmtexto (date-time)nãoHora da baixa informada pelo celular (igual a ocorridoEm). Não é o horário legal
recebidoEmtexto (date-time)nãoQuando o Veltra recebeu a baixa
recebedorobjetonãoSó o nome de quem recebeu. Ausente se não foi informado e para terceiro que não é o tomador
recebedor.nometextosimMáx. 120 caracteres. O documento do recebedor nunca vai no evento
coordenadaobjetonãoPosição do celular na baixa. Ausente sem posição e para terceiro que não é o tomador
coordenada.latitudenúmerosimmínimo: -90; máximo: 90
coordenada.longitudenúmerosimmínimo: -180; máximo: 180
coordenada.precisaoMnúmeronãomínimo: 0
quantidadeDeFotosinteironãoQuantas fotos o motorista anexou. As fotos ficam no pacote
canhotoobjetonãoOpcional. Terceiro que não é o tomador não recebe esta propriedade (D-11). Não traz a foto.
canhoto.chaveNfetextosimpadrão de formato: ^[0-9]{44}$
documentoIdtexto (uuid)sim
paradaIdtexto (uuid)sim
pdfobjetonãoOpcional. Terceiro que não é o tomador não recebe esta propriedade (D-11). Não traz o arquivo, só a referência.
pdf.comprovanteIdtexto (uuid)sim

ocorrencia.registrada#

Uma ocorrência foi registrada para um documento. O codigo é o código da ocorrência cadastrada na transportadora (até 20 caracteres). Quem recebe: TMS e TERCEIRO.

JSON
{
  "id": "018f6a2e-7c31-7a3e-8c2a-000000000007",
  "tipo": "ocorrencia.registrada",
  "versao": 1,
  "ocorridoEm": "2026-10-22T15:10:00Z",
  "ambiente": "PRODUCAO",
  "dados": {
    "ocorrenciaId": "55555555-5555-4555-8555-555555555555",
    "documentoId": "33333333-3333-4333-8333-333333333333",
    "codigo": "01"
  }
}
CampoTipoObrigatórioDescrição
codigotextosimmín. tamanho: 1; máx. tamanho: 20
documentoIdtexto (uuid)sim
ocorrenciaIdtexto (uuid)sim

comprovante.emitido#

O comprovante do documento foi emitido (selado). Use o comprovanteId para baixar o PDF e o pacote de evidências pela API de leitura (GET /v1/saida/comprovantes/{id}/pdf e .../pacote, só para a chave de integração do TMS). O comprovante só leva assinatura ICP-Brasil quando a transportadora ativou a validação opcional; veja o módulo 6. Quem recebe: TMS e TERCEIRO.

JSON
{
  "id": "018f6a2e-7c31-7a3e-8c2a-000000000008",
  "tipo": "comprovante.emitido",
  "versao": 1,
  "ocorridoEm": "2026-10-22T15:11:00Z",
  "ambiente": "PRODUCAO",
  "dados": {
    "comprovanteId": "66666666-6666-4666-8666-666666666666",
    "documentoId": "33333333-3333-4333-8333-333333333333"
  }
}
CampoTipoObrigatórioDescrição
canhotoobjetonãoOpcional. Terceiro que não é o tomador não recebe esta propriedade (D-11). Não traz a foto.
canhoto.chaveNfetextosimpadrão de formato: ^[0-9]{44}$
comprovanteIdtexto (uuid)sim
documentoIdtexto (uuid)sim
pdfobjetonãoOpcional. Terceiro que não é o tomador não recebe esta propriedade (D-11). Não traz o arquivo, só a referência.
pdf.comprovanteIdtexto (uuid)sim

evento_cte.estado_alterado#

Mudou o estado do evento 110180 (Comprovante de Entrega) que o Veltra acompanha. O estado vale um de: PENDENTE_DE_DADOS, AGUARDANDO_CTE_COMPLETO, AGUARDANDO_REVISAO, PENDENTE_DE_ENVIO, EM_TRANSMISSAO, REGISTRADO, REGISTRADO_COM_ALERTA, REJEITADO ou CANCELADO. protocolo e cStat aparecem só quando já existem. Quem recebe: TMS e TERCEIRO.

JSON
{
  "id": "018f6a2e-7c31-7a3e-8c2a-00000000000a",
  "tipo": "evento_cte.estado_alterado",
  "versao": 1,
  "ocorridoEm": "2026-10-22T15:30:00Z",
  "ambiente": "PRODUCAO",
  "dados": {
    "documentoId": "33333333-3333-4333-8333-333333333333",
    "estado": "REGISTRADO",
    "codigoEvento": 110180,
    "protocolo": "135260012345678",
    "cStat": 135
  }
}
CampoTipoObrigatórioDescrição
cStatinteironãomínimo: 100; máximo: 999
codigoEventoconstantesimvalor fixo: 110180
documentoIdtexto (uuid)sim
estadoenumsimvalores: PENDENTE_DE_DADOS, AGUARDANDO_CTE_COMPLETO, AGUARDANDO_REVISAO, PENDENTE_DE_ENVIO, EM_TRANSMISSAO, REGISTRADO, REGISTRADO_COM_ALERTA, REJEITADO, CANCELADO
protocolotextonãoProtocolo devolvido pela SEFAZ, quando já existe.; padrão de formato: ^[0-9]{15}$

posicao.lote#

Lote de posições de um veículo. É agregado: o Veltra nunca manda um aviso por ponto de GPS. A cada 5 minutos ele fecha uma janela por placa, com no máximo 60 pontos (se houver mais, escolhe pontos espalhados pela janela), e ignora posições simuladas. Só o assinante TMS recebe. Com a cadência de 10 ou 15 minutos, o envio espera o próximo múltiplo da cadência; a janela continua sendo de 5 minutos. Hoje o evento é gerado só para Produção.

JSON
{
  "id": "018f6a2e-7c31-7a3e-8c2a-00000000000c",
  "tipo": "posicao.lote",
  "versao": 1,
  "ocorridoEm": "2026-10-22T15:00:00Z",
  "ambiente": "PRODUCAO",
  "dados": {
    "placa": "ABC1D23",
    "janelaInicio": "2026-10-22T14:55:00Z",
    "janelaFim": "2026-10-22T15:00:00Z",
    "pontos": [
      {
        "lat": -23.5505,
        "lon": -46.6333,
        "precisaoM": 12,
        "registradoEm": "2026-10-22T14:56:10Z"
      },
      {
        "lat": -23.5521,
        "lon": -46.635,
        "precisaoM": 9,
        "registradoEm": "2026-10-22T14:58:40Z"
      }
    ]
  }
}
CampoTipoObrigatórioDescrição
janelaFimtexto (date-time)sim
janelaIniciotexto (date-time)sim
placatextosimmín. tamanho: 7; máx. tamanho: 8
pontoslistasimmáx. itens: 60
pontos[]objetonão
pontos[].latnúmerosimmínimo: -90; máximo: 90
pontos[].lonnúmerosimmínimo: -180; máximo: 180
pontos[].precisaoMnúmerosimmínimo: 0
pontos[].registradoEmtexto (date-time)sim

veiculo.km_registrado#

O motorista informou o km do hodômetro na saída da base (momento SAIDA) ou na chegada (momento CHEGADA). Só o assinante TMS recebe; o cadastro de um TERCEIRO com esse tipo é recusado. Na chegada, kmSaida e kmRodados vêm quando o km de saída é conhecido e a chegada não é menor que a saída. O mesmo km aparece em kmSaida, kmChegada e kmRodados de GET /v1/saida/manifestos. O Veltra não reenvia o histórico anterior à versão que criou este evento.

JSON
{
  "id": "018f6a2e-7c31-7a3e-8c2a-000000000013",
  "tipo": "veiculo.km_registrado",
  "versao": 1,
  "ocorridoEm": "2026-10-22T21:30:00Z",
  "ambiente": "PRODUCAO",
  "dados": {
    "saidaId": "55555555-5555-4555-8555-555555555555",
    "momento": "CHEGADA",
    "placa": "ABC1D23",
    "manifestoIds": ["11111111-1111-4111-8111-111111111111"],
    "km": 120450,
    "kmSaida": 120000,
    "kmChegada": 120450,
    "kmRodados": 450
  }
}
CampoTipoObrigatórioDescrição
saidaIdtexto (uuid)simIdentifica a viagem (saída da base)
momentotextosimSAIDA ou CHEGADA
placatextosimPlaca do veículo
manifestoIdslista de uuidsimManifestos da viagem
kminteirosimKm informado neste momento
kmSaidainteironãoKm de saída da viagem
kmChegadainteironãoKm de chegada (só em CHEGADA)
kmRodadosinteironãokmChegada - kmSaida (só em CHEGADA, com saída conhecida)

4. O que chega: cabeçalhos e corpo de cada entrega#

Cada entrega é um POST HTTP/1.1 com o corpo JSON do evento e estes cabeçalhos:

CabeçalhoConteúdo
Content-Typeapplication/json
User-AgentVeltra-Webhook/1
webhook-ididentificador da entrega, no formato msg_ seguido de um UUID. É o mesmo em todas as retentativas e no reenvio manual.
webhook-timestampsegundos Unix (um número inteiro) do momento desta tentativa. Muda a cada tentativa.
webhook-signaturev1, seguido do HMAC-SHA256 em Base64. Na rotação do segredo, duas assinaturas separadas por espaço.

Esse é o formato do Standard Webhooks, conforme o ADR 0022. Veja como a chamada chega ao seu servidor (os valores de webhook-signature e o corpo são de exemplo):

HTTP
POST /webhooks/veltra HTTP/1.1
Host: SEU_ENDERECO_AQUI
Content-Type: application/json
Content-Length: 302
Connection: close
User-Agent: Veltra-Webhook/1
webhook-id: msg_018f6a2e-7c31-4a3e-8c2a-0000000000aa
webhook-timestamp: 1792681203
webhook-signature: v1,ASSINATURA_EM_BASE64

{"id":"018f6a2e-7c31-7a3e-8c2a-000000000006","tipo":"baixa.registrada","versao":1,"ocorridoEm":"2026-10-22T15:00:00Z","ambiente":"PRODUCAO","dados":{"baixaId":"44444444-4444-4444-8444-444444444444","documentoId":"33333333-3333-4333-8333-333333333333","paradaId":"22222222-2222-4222-8222-222222222222"}}

Os nomes de cabeçalho são minúsculos aqui; HTTP não diferencia maiúsculas de minúsculas, e o seu servidor pode entregá-los de outro jeito (por exemplo, Webhook-Id).

A string assinada é, em bytes UTF-8:

Texto
{webhook-id} + "." + {webhook-timestamp} + "." + {corpo exato como chegou}

O HMAC-SHA256 dessa string, com a chave do seu segredo, em Base64, é o que vem depois de v1,.

O Veltra assina os bytes que ele guardou ao criar a entrega e reenvia exatamente esses bytes em cada tentativa. Ele não monta o JSON de novo. Por isso a ordem dos campos e os espaços do exemplo acima podem ser diferentes dos que você vê: o que vale é o que chegou.

5. Como verificar a assinatura#

Verifique sempre, antes de processar. Sem isso, qualquer pessoa que descubra o seu endereço poderia enviar eventos falsos.

Os seis passos, na ordem:

Leia o corpo em bytes, sem mexer

Use o corpo cru da requisição. Se o seu framework já fez parse do JSON e você serializa de novo, a ordem dos campos, os espaços e a codificação podem mudar, e a assinatura deixa de conferir. Configure uma leitura "raw" nessa rota (os exemplos abaixo mostram como).

Confira o relógio

Recuse se webhook-timestamp estiver a mais de 5 minutos (300 segundos) do relógio do seu servidor, para trás ou para a frente. Isso impede que alguém reaproveite uma chamada antiga que conseguiu copiar. Mantenha o relógio do servidor sincronizado (NTP). A tolerância de 5 minutos é a recomendação deste guia; o Veltra não a impõe.

Monte a string assinada

webhook-id, um ponto, webhook-timestamp, um ponto, e o corpo.

Obtenha os bytes da chave

O segredo tem o formato whsec_ + Base64. Tire o prefixo whsec_ e decodifique o Base64: os bytes resultantes são a chave do HMAC. Um erro comum é usar o texto do segredo, com o prefixo, como se fosse a chave.

Calcule o HMAC-SHA256 e compare em tempo constante

Compare o resultado com cada assinatura que começa com v1, no cabeçalho (separadas por espaço), decodificando a parte em Base64. Use a função de comparação em tempo constante da linguagem (não use ==), porque uma comparação comum pode vazar, pelo tempo de resposta, quantos bytes da assinatura estavam certos.

Se nenhuma conferir, recuse

Responda 401 e não processe. Atenção ao efeito no Veltra: um 401 não é retentado e conta como entrega esgotada (seção 6). Se o seu segredo estiver errado, os eventos vão para a fila de falhas.

As cinco implementações abaixo seguem exatamente esses passos. Os valores de segredo nos exemplos são sempre o seu (SEU_SEGREDO_AQUI), nunca um valor copiado de outro lugar.

// Node 18 ou superior. npm install express
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const SEGREDO = process.env.VELTRA_WEBHOOK_SEGREDO; // "whsec_SEU_SEGREDO_AQUI", guardado em cofre
const TOLERANCIA_SEGUNDOS = 300;

export function verificar(segredo, id, timestamp, corpo, cabecalho) {
  // corpo: Buffer com os bytes crus
  if (!/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCIA_SEGUNDOS) return false;
  const chave = Buffer.from(segredo.replace(/^whsec_/, ''), 'base64');
  const esperado = createHmac('sha256', chave)
    .update(`${id}.${timestamp}.`)
    .update(corpo)
    .digest();
  return cabecalho.split(' ').some((parte) => {
    if (!parte.startsWith('v1,')) return false;
    const recebido = Buffer.from(parte.slice(3), 'base64');
    return recebido.length === esperado.length && timingSafeEqual(esperado, recebido);
  });
}

const app = express();

// express.raw entrega req.body como Buffer, sem mexer nos bytes.
app.post('/webhooks/veltra', express.raw({ type: '*/*', limit: '1mb' }), (req, res) => {
  const id = req.get('webhook-id');
  const timestamp = req.get('webhook-timestamp');
  const assinatura = req.get('webhook-signature');
  if (!id || !timestamp || !assinatura || !verificar(SEGREDO, id, timestamp, req.body, assinatura)) {
    return res.status(401).end();
  }
  res.status(200).end(); // responda logo...
  const evento = JSON.parse(req.body.toString('utf8'));
  setImmediate(() => processar(id, evento)); // ...e processe depois
});

function processar(webhookId, evento) {
  // sua regra de negócio; use webhookId para não processar duas vezes (seção 8)
}

app.listen(3000);

6. O que o seu endereço deve responder e o que o Veltra considera falha#

Regra de ouro: responda 2xx em poucos segundos e processe depois. Receber é uma coisa; processar é outra. Grave o evento (num banco ou numa fila) e devolva 200 ou 204.

O Veltra espera até 5 segundos para conectar e até 10 segundos para ler a resposta. Passou disso, é falha. Ele lê no máximo os primeiros 4 KB do corpo da resposta e ignora o conteúdo: só o código importa.

O que o seu endereço fazO que o Veltra entendeO que acontece
Responde 2xxEntregueFim. A entrega fica como "Entregue".
Responde 429 ou 5xx (500, 502, 503, 504...)Falha temporáriaNova tentativa mais tarde (seção 7). Se vier o cabeçalho Retry-After maior que o próximo intervalo, o Veltra espera esse tempo.
Não responde, demora mais que o limite, a conexão cai, o DNS não resolve ou o certificado HTTPS é inválidoFalha temporáriaNova tentativa mais tarde.
Responde 410 (Gone)"Este endereço não existe mais"A entrega esgota na hora, o assinante é pausado e nada mais é enviado até alguém retomar.
Responde 3xx (redirecionamento)Falha definitivaNão segue o redirecionamento. A entrega esgota na hora. Cadastre o endereço final.
Responde outro 4xx (400, 401, 403, 404, 422...)Falha definitivaSem nova tentativa: a entrega esgota na hora e vai para a fila de falhas.

Consequências práticas:

  • Para "tente de novo mais tarde" (banco fora do ar, fila cheia), responda 503, não 400.
  • Um 401 por assinatura inválida é o certo para um intruso, mas, se o problema for o seu segredo errado, cada evento legítimo vira uma entrega esgotada. Veja a regra das 20 entregas esgotadas na seção 7.
  • Não faça redirecionamento (http para https, ou www): cadastre a URL final.
  • O certificado HTTPS precisa ser de uma autoridade pública reconhecida e valer para o nome do servidor. Certificado autoassinado ou vencido é falha temporária (a entrega será tentada de novo até a janela acabar).

7. Retentativa, fila de falhas e reenvio#

Intervalos exatos#

Quando uma entrega falha de forma temporária, o Veltra tenta de novo com intervalos crescentes. A primeira tentativa sai assim que o evento é distribuído (normalmente em segundos). Depois de cada falha:

Falha númeroEspera até a próxima tentativa
11 minuto
25 minutos
330 minutos
42 horas
512 horas
6 em diante12 horas

Cada espera sofre uma variação aleatória de 80% a 120% (por exemplo, o intervalo de 1 minuto fica entre 48 e 72 segundos), para que muitas entregas atrasadas não voltem todas no mesmo instante.

Se a resposta trouxer Retry-After (em segundos ou em data HTTP) maior que o intervalo da tabela, vale o Retry-After, limitado a 72 horas. Um Retry-After menor que o intervalo da tabela é ignorado. Se a data da próxima tentativa, com o Retry-After, passar do fim da janela de 72 horas, a entrega esgota em vez de esperar.

Quando desiste#

O Veltra desiste quando a próxima tentativa cairia depois de 72 horas contadas desde a primeira tentativa. Com a tabela acima isso dá cerca de 10 tentativas. Nesse momento a entrega passa para "Tentativas esgotadas" (estado ESGOTADA) e vai para a fila de falhas (em inglês, dead letter): um depósito das entregas que não chegaram, que não são apagadas e podem ser reenviadas.

Uma entrega também vai direto para a fila de falhas, sem esperar as 72 horas, quando a falha é definitiva: 410, redirecionamento, outro 4xx, endereço recusado pela proteção da seção 10, ou URL em esquema não permitido.

Quando o assinante é pausado ou desativado#

  • 410: o assinante vai para Pausado. Os eventos novos deixam de gerar entrega até um administrador retomar. Enquanto o assinante estiver Pausado, nenhuma entrega sai. Os eventos desse período não são apagados e são distribuídos quando ele voltar a Ativo, sem garantia de ordem (seção 8). Para ter certeza de que nada ficou para trás, reconcilie pela consulta (seção 14).
  • 20 entregas esgotadas seguidas: o assinante vai para Desativado automaticamente e o Veltra envia um e-mail ao contato técnico cadastrado ("Webhook desativado por falhas seguidas"). "Seguidas" quer dizer que as 20 últimas entregas concluídas foram todas esgotadas, sem nenhuma entregue no meio. Uma única entrega bem-sucedida zera a contagem.

Reenviar pelo portal#

Em Integrações > Webhooks, abra o webhook e use a aba Fila de falhas. Cada linha tem Reenviar, e no topo há Reenviar todas (N). Corrija antes o problema que fez as entregas falharem (endereço, segredo, servidor fora do ar), senão elas voltam para a fila.

Reenviar pela API#

curl
curl -X POST \
  -H "Authorization: Bearer SUA_CHAVE_AQUI" \
  https://SEU_ENDERECO_AQUI/v1/integracoes/webhooks/ID_DO_WEBHOOK/entregas/ID_DA_ENTREGA/reenvio

Nesse exemplo, TOKEN_DO_PORTAL representa o token do login do administrador no portal (esta rota não usa a chave de API do TMS). A rota é só de administrador e responde 202. Se a entrega não for deste webhook, responde 404. O que o reenvio faz:

  • reabre a mesma entrega: o webhook-id e o corpo (os mesmos bytes) não mudam;
  • zera a contagem de tentativas e recomeça a janela de 72 horas;
  • na próxima tentativa, assina de novo, com um webhook-timestamp novo e o segredo atual.

O reenvio só sai enquanto o assinante estiver Ativo. Um assinante pausado só envia depois de retomado. Um assinante desativado não envia (crie um novo, seção 13).

8. Idempotência, ordem e duplicatas#

Duplicatas acontecem e são normais#

O Veltra garante entregar pelo menos uma vez. Um caso típico: o seu servidor processou o evento, mas a resposta 200 se perdeu na rede. Para o Veltra foi uma falha, e ele manda de novo. O mesmo vale para um reenvio manual e para a consulta incremental.

Duas identificações, dois usos#

IdentificaçãoOnde estáMuda entre tentativas?Serve para
webhook-idcabeçalhonão (igual em retentativas e reenvios)deduplicar entregas deste webhook
id do envelopecorponãoidentificar o evento; é o mesmo id que a consulta /v1/saida/eventos devolve

Use o webhook-id para deduplicar o que chega por webhook. Use o id do envelope quando o mesmo evento puder chegar por dois caminhos (webhook e consulta) ou para mais de um webhook seu.

Como deduplicar#

  1. Mantenha uma tabela com o identificador como chave única.
  2. Ao receber, tente inserir o webhook-id. Se a inserção funcionar, o evento é novo: processe.
  3. Se a inserção falhar por chave repetida, o evento já foi visto: responda 2xx sem processar de novo.
  4. Guarde os identificadores por pelo menos 7 dias (a janela de retentativa é de 72 horas, e um reenvio manual pode vir depois).

Faça o passo 2 e o processamento na mesma transação do banco, para que uma queda no meio não deixe o evento marcado como visto sem ter sido processado.

Ordem#

A ordem não é garantida. Uma entrega que falhou e foi retentada chega depois de outra mais nova. Por isso:

  • não assuma que baixa.registrada chega antes de comprovante.emitido;
  • compare o ocorridoEm para saber qual fato é mais recente (útil para o estado de evento_cte.estado_alterado);
  • se um evento referenciar algo que você ainda não conhece (um documentoId novo), guarde o evento e reprocesse depois, ou consulte o documento na API.

9. O segredo: giro sem perder eventos#

O Veltra guarda o segredo cifrado e assina cada tentativa com ele. Gire o segredo se ele vazou, se alguém que o conhecia saiu da equipe, ou periodicamente.

Gire o segredo

No portal, na página do webhook, clique em Girar segredo e confirme. Pela API, POST /v1/integracoes/webhooks/{id}/rotacao-segredo. O novo segredo aparece uma única vez; copie e guarde.

Entenda a janela de 24 horas

A partir do giro, e por 24 horas, cada entrega leva duas assinaturas no cabeçalho webhook-signature, separadas por espaço: primeiro a do segredo novo, depois a do antigo. O exemplo:

v1,ASSINATURA_DO_SEGREDO_NOVO v1,ASSINATURA_DO_SEGREDO_ANTIGO

O código da seção 5 já percorre todas as assinaturas v1 do cabeçalho: quem ainda tem o segredo antigo continua conferindo, e quem já trocou confere pela primeira.

Troque o segredo no seu sistema

Dentro das 24 horas, atualize a variável ou o cofre com o segredo novo e reinicie o receptor. Nenhum evento é perdido nem recusado nesse intervalo.

Depois das 24 horas

Só a assinatura do segredo novo é enviada. O segredo antigo deixa de funcionar.

Detalhes importantes:

  • Se você girar duas vezes dentro de 24 horas, o segredo que fica como "anterior" é o do penúltimo giro, e o primeiro segredo deixa de valer na hora. Evite girar de novo antes de terminar a troca.
  • Se o segredo vazou, não espere. Um giro deixa o segredo vazado valendo por mais 24 horas; para cortá-lo já, gire duas vezes seguidas (o vazado deixa de valer) e atualize logo o receptor com o último segredo, porque o penúltimo vale só por 24 horas.
  • A edição do webhook (nome, URL, tipos) não gira o segredo.

10. Segurança#

  • HTTPS em Produção. O Veltra só entrega em https para webhooks de Produção. O http é aceito só em webhooks de Teste.
  • Verifique sempre a assinatura e o relógio (seção 5). É a prova de que a chamada veio do Veltra e não foi adulterada nem repetida.
  • Não confie só no endereço IP de origem. Não existe uma lista de IPs para você liberar; a assinatura é a prova. Se a sua política de rede exige liberar IPs, combine isso com a verificação, nunca no lugar dela.
  • Guarde o segredo em cofre. Nunca em repositório, em código ou em log. Registre em log o webhook-id e o id do evento, jamais o segredo nem a assinatura completa.
  • Limite o tamanho do corpo que o seu endpoint aceita (os eventos são pequenos; 1 MB sobra).
  • Trate o conteúdo como dado. Valide o JSON e os tipos antes de usar. Ignore campos que você não conhece (mudanças futuras são aditivas).
  • Dados pessoais. Motoristas, destinatários e localização são dados pessoais: trate-os conforme a LGPD nos seus sistemas e não os grave em logs abertos.

Proteção do lado do Veltra contra SSRF. SSRF é quando alguém cadastra como "endereço do webhook" um endereço interno (de uma rede privada) para fazer o servidor do Veltra chamar um lugar onde não deveria. Para impedir isso, o Veltra:

  • a cada entrega, resolve o nome (DNS) uma única vez e confere o IP: se qualquer endereço for local (loopback), da rede privada (10.x, 172.16.x a 172.31.x, 192.168.x), de link-local (inclui o 169.254.169.254 dos serviços de nuvem), de CGNAT (100.64.x a 100.127.x), multicast, de rede privada IPv6 (fc00::/7), ou um IPv4 escondido dentro de IPv6, a conexão nem abre;
  • liga a conexão direto no IP já conferido, o que impede a troca do IP entre a conferência e a conexão (ataque de "DNS rebinding");
  • não segue redirecionamentos;
  • lê no máximo 4 KB da resposta e usa limites de tempo de 5 e 10 segundos;
  • só aceita https em Produção.

Em webhooks de Teste, o Veltra permite http e o endereço local (localhost) para facilitar o desenvolvimento. As demais proteções valem igual.

11. Como testar#

Passo 1: o botão de teste#

Na página do webhook, clique em Enviar teste. O Veltra envia um evento de teste ao seu endereço, marcado como Teste, e mostra o resultado: "Teste entregue: o assinante respondeu 200 em N ms" ou "O teste falhou: motivo". Pela API: POST /v1/integracoes/webhooks/{id}/teste, que devolve entregue, codigoHttp, duracaoMs e, se falhou, motivo. O evento de teste tem o tipo webhook.teste e "ambiente": "TESTE", é assinado como uma entrega real e vai ao endereço do próprio webhook; não é um evento de negócio, não entra no histórico de entregas e não conta para a desativação por falhas seguidas. As regras de segurança do endereço valem iguais: se o endereço for interno, o teste volta com entregue: false e o motivo ENDERECO_RECUSADO.

Passo 2: desenvolver na sua máquina com um túnel#

O Veltra precisa alcançar o seu endereço pela internet, então o localhost do seu notebook não serve em Produção. Use uma ferramenta de túnel que dá a você um endereço https público apontando para a sua máquina. Algumas opções gratuitas, sem preferência entre elas: Cloudflare Tunnel (cloudflared), ngrok e localtunnel. Em todas, você roda o seu receptor (por exemplo na porta 3000), abre o túnel para essa porta e cadastra no Veltra o endereço https que a ferramenta mostrar. Cuidado: o endereço do túnel é público, então só o deixe aberto enquanto estiver testando, e confira a assinatura mesmo assim.

Em um webhook de Teste (ambiente de Teste), o http e o localhost são aceitos; o túnel só é necessário quando o seu receptor roda em outra rede.

Passo 3: testar o seu receptor sem o Veltra#

Você pode assinar uma chamada você mesmo e disparar contra o seu receptor local. Com openssl e curl:

curl
# 1. Crie um segredo de teste e mostre-o no formato whsec_ (use só para testes locais)
CHAVE_HEX=$(openssl rand -hex 32)
echo "whsec_$(printf '%s' "$CHAVE_HEX" | xxd -r -p | base64)"   # configure este valor no receptor

# 2. Monte o corpo, o id e o timestamp
echo '{"id":"018f6a2e-7c31-7a3e-8c2a-000000000006","tipo":"baixa.registrada","versao":1,"ocorridoEm":"2026-10-22T15:00:00Z","ambiente":"TESTE","dados":{"baixaId":"44444444-4444-4444-8444-444444444444","documentoId":"33333333-3333-4333-8333-333333333333","paradaId":"22222222-2222-4222-8222-222222222222"}}' > corpo.json
ID="msg_teste_0001"
TS=$(date +%s)

# 3. Assine: HMAC-SHA256 de "id.timestamp.corpo", em Base64
ASSINATURA=$( { printf '%s.%s.' "$ID" "$TS"; cat corpo.json; } \
  | openssl dgst -sha256 -mac HMAC -macopt hexkey:"$CHAVE_HEX" -binary | openssl base64 -A)

# 4. Envie ao receptor local
curl -i -X POST http://localhost:3000/webhooks/veltra \
  -H "Content-Type: application/json" \
  -H "webhook-id: $ID" \
  -H "webhook-timestamp: $TS" \
  -H "webhook-signature: v1,$ASSINATURA" \
  --data-binary @corpo.json

O receptor deve responder 200. Altere um caractere de corpo.json depois de assinar, ou espere mais de 5 minutos, e ele deve responder 401. Esses dois testes provam que a verificação de corpo e de relógio funciona. Rode também o mesmo envio duas vezes e confira que o segundo é reconhecido como repetido (seção 8).

Roteiro de aceite antes de ir para Produção#

  1. O teste do botão voltou 2xx.
  2. Um corpo alterado é recusado com 401.
  3. Um webhook-timestamp antigo é recusado.
  4. Um webhook-id repetido não é processado duas vezes.
  5. O seu servidor responde em poucos segundos mesmo com o banco lento (processamento depois da resposta).
  6. A troca de segredo foi ensaiada (seção 9) sem perder evento.

12. Monitoramento: o que ver no portal#

Em Integrações > Webhooks:

  • Lista: colunas Nome, URL, Eventos, Situação, Última entrega e as ações rápidas (Enviar teste, Pausar ou Retomar, ver detalhes).
  • Situação do assinante: Ativo (recebendo), Pausado (você pausou, ou o endereço respondeu 410) e Desativado automaticamente (20 entregas esgotadas seguidas). O contrato também prevê "Com falhas", que o Veltra ainda não aplica.
  • Resumo: URL, quem recebe, e-mail de contato, raízes de CNPJ, cadência, data de criação e data da última troca de segredo.
  • Histórico de entregas: uma linha por entrega, com Horário, Evento, ID da entrega (o identificador da entrega no Veltra, com botão de copiar; é o {entregaId} da rota de reenvio e não é o cabeçalho webhook-id), Situação, Código HTTP, Tentativas e Duração.
  • Fila de falhas: só as entregas esgotadas, com Reenviar e Reenviar todas (N).

Situações de uma entrega: Entregue, Aguardando nova tentativa, Falhou, Teste e Tentativas esgotadas.

Rotina sugerida: olhe a fila de falhas depois de qualquer manutenção do seu servidor; mantenha o e-mail de contato técnico em uma caixa que alguém lê (é por ele que o aviso de desativação chega); e acompanhe do seu lado o tempo de resposta do endpoint.

13. Solução de problemas#

SintomaCausa provávelCorreção
Todas as entregas aparecem como "Tentativas esgotadas" com código 401Segredo errado, ou corpo alterado antes da verificaçãoConfira se está usando o segredo do webhook certo, com o prefixo tirado e o Base64 decodificado. Leia o corpo cru. Corrija, depois use Reenviar todas.
401 só em alguns eventosParse e nova serialização do JSON no framework antes de verificarConfigure leitura do corpo em bytes na rota do webhook (seção 5, passo 1).
Funcionou ontem, hoje recusa tudo por tempoRelógio do seu servidor atrasado ou adiantado mais de 5 minutosSincronize o relógio (NTP).
Verificação falha logo depois de girar o segredoReceptor ainda com o segredo antigo depois das 24 horas, ou você girou duas vezesUse o segredo mais recente; se perdeu, gire de novo e guarde.
Webhook "Pausado" sem você ter pausadoO endereço respondeu 410Corrija o endereço ou faça o servidor parar de responder 410, depois Retomar.
Webhook "Desativado automaticamente"20 entregas esgotadas seguidasCorrija a causa. Um webhook desativado some da lista e não volta com Retomar: crie um novo webhook e configure o novo segredo no receptor.
Entrega esgotada na hora, sem 10 tentativas, com 404 ou 400Rotas e erros 4xx não são retentadosCorrija a rota e use Reenviar. Para "tente depois", responda 503.
Entrega esgotada na hora, sem código HTTPEndereço recusado (rede interna), http em Produção, ou redirecionamentoCadastre um endereço https público e final, sem redirecionamento.
Muitas retentativas com "Falhou" e código vazioSeu servidor não responde em 10 segundos, DNS ou certificado HTTPS inválidoResponda rápido e processe depois; confira DNS e a cadeia do certificado.
O mesmo evento chega duas vezesNormal: resposta perdida, retentativa ou reenvioDeduplique pelo webhook-id (seção 8).
Eventos chegam fora de ordemNormal: uma retentativa foi ultrapassadaUse ocorridoEm para ordenar.
Não chega posicao.lotePapel TERCEIRO, ambiente de Teste, ou o veículo não enviou posições na janelaSó TMS recebe; em Teste o evento não é gerado; confira a cadência escolhida.
Não chega nenhum evento, e o assinante está AtivoEventos de outro tipo, de outro ambiente (Teste e Produção são separados) ou o tipo ainda não é disparadoConfira os tipos marcados e a coluna "Gerado hoje pelo Veltra" (seção 3).
O evento de teste voltou "O teste falhou"Endereço inalcançável a partir da internetUse um túnel ou publique o endereço (seção 11).
Quero o PDF e ele não vem no webhookO webhook leva só o comprovanteIdBaixe pela API com a chave de integração (seção 14).

14. Relação com a API de consulta#

Mapa interativo de todas as rotas de integração: Swagger da API. O guia explicado de cada rota está em api-por-rota.mdx.

O ADR 0013 fixa a regra de paridade: o que o portal mostra existe também na API de leitura, nos eventos de webhook e na exportação. Na prática:

  • Os eventos de webhook e os eventos da consulta GET /v1/saida/eventos usam o mesmo envelope e o mesmo id. O que chega por um caminho pode ser conferido pelo outro.
  • O webhook é o caminho rápido; a consulta é o caminho completo e reprocessável. Se o seu servidor ficou fora do ar por mais de 72 horas, ou se você perdeu um evento, a consulta recupera.
  • Para detalhes que o webhook não traz (o documento inteiro, o PDF, o pacote de evidências, o XML do evento 110180), use a API de leitura com a chave do TMS. A referência de leitura lista as rotas.

Receita recomendada (rede de segurança):

  1. Receba pelo webhook e processe com deduplicação.
  2. A cada hora (ou a cada dia, conforme o seu volume), chame GET /v1/saida/eventos?alteradoDesde=CURSOR_GUARDADO_AQUI e processe o que ainda não viu, comparando pelo id do evento.
  3. Guarde o proximoCursor devolvido.
curl
curl -H "X-Veltra-Api-Key: SUA_CHAVE_AQUI" \
  "https://SEU_ENDERECO_AQUI/v1/saida/eventos?alteradoDesde=CURSOR_GUARDADO_AQUI&limite=200"

Lembretes da consulta: o cursor é opaco (não monte nem edite); o Veltra só devolve eventos alterados há pelo menos 5 segundos; e as chaves de consulta de embarcador e destinatário não recebem posicao.lote.

15. Resumo para colar na parede#

  • O webhook é um POST do Veltra para o seu endereço. Responda 2xx rápido, processe depois.
  • Três cabeçalhos: webhook-id, webhook-timestamp, webhook-signature.
  • Assinatura: HMAC-SHA256 de id.timestamp.corpo, chave = Base64 do segredo sem whsec_, comparação em tempo constante, tolerância de 5 minutos.
  • 429, 5xx e falha de rede são retentados (1 min, 5 min, 30 min, 2 h, 12 h, 12 h...) por até 72 horas. 4xx (menos 429) e 3xx não são retentados; 410 pausa o webhook.
  • Duplicata é normal: deduplique pelo webhook-id. Ordem não é garantida.
  • Giro de segredo: 24 horas com duas assinaturas.
  • Rede de segurança: consulta incremental com alteradoDesde.