Pular para o conteúdo
Veltra Ajuda
Manual da APIPlaybookEntrar

Referência da API

Manual de criação de webhook

Abrir o Swagger

Manual de criação de webhook

Este é o passo a passo completo para criar um webhook no Veltra e receber os eventos com segurança: do cadastro no portal até o receptor funcionando em produção. Ele junta, em ordem de uso, o que está espalhado na referência de webhooks e eventos e no módulo 5. Para o detalhe de cada campo, a referência é a fonte completa.

Tudo aqui vem do contrato da API (contracts/openapi), do contrato dos eventos (contracts/webhooks) e do código do Veltra. Onde o produto ainda não tem algo, a página diz isso de forma explícita.

1. O que é um webhook e quando usar#

Um webhook é um aviso automático. Quando algo acontece no Veltra (o motorista chega numa parada, uma baixa é registrada, um comprovante é emitido), o Veltra faz um POST HTTP para um endereço do seu sistema, com os dados do fato em JSON.

Quero...Use
Ser avisado em segundos, sem ficar perguntandoWebhook
Buscar um dado específico agora (um documento, o PDF, o pacote de evidências)API de leitura (GET /v1/saida/...) com a chave de integração
Recuperar o que perdi (servidor fora do ar, evento esquecido)API de leitura com alteradoDesde

A recomendação é usar os dois: o webhook para a velocidade e a consulta incremental como rede de segurança (veja a referência). O webhook leva identificadores e dados curtos, nunca arquivos: para o PDF do comprovante você recebe o comprovanteId e baixa o arquivo pela API.

A entrega é "pelo menos uma vez": o mesmo evento pode chegar mais de uma vez, e a ordem de chegada não é garantida. O receptor precisa estar preparado para isso (seção 6).

2. Antes de começar#

Quem pode criar#

  • Só o perfil ADMIN da transportadora cria e gerencia webhooks. O contrato registra: "Perfil ADMIN. O perfil EMBARCADOR não acessa esta rota."
  • No código dos webhooks não há exigência de módulo contratado: qualquer transportadora com perfil ADMIN acessa a tela.

Ambiente: Teste ou Produção#

Cada webhook pertence a um ambiente e ele não muda depois de criado.

TesteProdução
Endereço http://aceitorecusado (só https://)
Endereço local (localhost, 127.0.0.1)aceitorecusado
Rede privada (10.x, 192.168.x, 172.16.x a 172.31.x) e 169.254.xrecusadorecusado
Eventos que chegamsó os gerados no ambiente de Testesó os gerados em Produção
posicao.lotenão é gerado hojegerado

As proteções contra endereços internos valem nos dois ambientes (veja a seção 10 da referência); a única exceção é o loopback (localhost), liberado em Teste.

O que você precisa ter pronto#

  1. Um endereço do seu sistema que aceite POST com corpo JSON e responda 2xx em poucos segundos. Em Produção: https:// com certificado de uma autoridade pública válida, sem redirecionamento.
  2. Um lugar seguro para guardar o segredo (cofre de segredos ou variável de ambiente protegida).
  3. Um e-mail de contato técnico que alguém lê (é por ele que o Veltra avisa se o webhook for desativado).
  4. Decidir quem recebe: TMS (o sistema da transportadora; recebe tudo, inclusive posicao.lote e veiculo.km_registrado) ou TERCEIRO (embarcador ou cliente, limitado às raízes de CNPJ que você informar).

3. Passo a passo no portal#

Esta página não tem prints do formulário de webhook: a pasta de imagens da central ainda não tem capturas dessa tela. Os nomes de campos e botões abaixo foram conferidos no código da tela.

Abra Integrações > Webhooks

No menu lateral, grupo Integrações, item Webhooks (/integracoes/webhooks). A lista mostra Nome, URL, Eventos, Situação e Última entrega. Se ainda não houver nenhum, a tela mostra "Nenhum webhook criado".

Clique em Criar webhook e preencha o formulário

  • Nome do assinante: de 1 a 120 caracteres, por exemplo "TMS da matriz".
  • Ambiente: Teste ou Produção. Escolha com cuidado: não muda depois. Comece por Teste.
  • Quem recebe: "Sistema da transportadora (TMS)" ou "Terceiro (embarcador, cliente)".
  • URL de destino: o endereço do seu receptor. Em Produção precisa começar com https://; em Teste, http:// também vale.
  • Eventos: marque os tipos que quer receber (ao menos um). Marque só o que vai usar.
  • Enviar posição a cada: aparece só para TMS com posicao.lote: 5, 10 ou 15 minutos.
  • CNPJ do terceiro: só para TERCEIRO. Informe a raiz (8 caracteres) ou o CNPJ inteiro; o portal guarda a raiz.
  • E-mail de contato técnico: obrigatório.

Salve e copie o segredo na hora

Ao salvar, o portal abre a janela "Guarde o segredo agora". O segredo aparece uma única vez, no formato whsec_ seguido de texto em Base64 (32 bytes aleatórios). Use Copiar segredo, guarde-o inteiro (com o prefixo) no cofre e só então marque "Já guardei o segredo" e clique em Concluir. Fechar a janela sem marcar pede confirmação. Se perder o segredo, não há como recuperá-lo: gere outro com Girar segredo (seção 11).

Como guardar: em um cofre de segredos ou variável de ambiente do servidor do receptor (por exemplo VELTRA_WEBHOOK_SEGREDO). Nunca em repositório, em código-fonte, em chat ou em log.

Configure o receptor

Implemente o receptor com a verificação de assinatura da seção 7 e coloque o segredo nele. Publique o receptor antes de marcar eventos de produção.

Envie um teste

Na página do webhook, clique em Enviar teste e confira o resultado (seção 9). Só depois disso trabalhe com eventos reais.

Regras que o cadastro aplica (recusa com erro se descumpridas):

  • Nome de 1 a 120 caracteres; ao menos um tipo de evento, e só os 8 do catálogo da seção 5.
  • posicao.lote e veiculo.km_registrado: só para TMS. Um TERCEIRO com esses tipos é recusado.
  • TERCEIRO exige ao menos uma raiz de CNPJ (8 caracteres, letras ou números).
  • Cadência de posição (5, 10 ou 15) só vale para TMS com posicao.lote.
  • Em Produção, a URL precisa ser https, com nome de servidor.
  • O e-mail de contato precisa ser válido (até 320 caracteres).
  • Depois de criado, você pode editar nome, URL, tipos, papel, escopo, cadência e e-mail. Ambiente não muda. Mudar a URL não muda o segredo.

4. Criar o assinante pela API#

A mesma criação existe na API do portal, para quem prefere automatizar. Atenção: estas rotas não usam a chave de integração do TMS (X-Veltra-Api-Key). Elas usam o JWT de um usuário ADMIN (Authorization: Bearer), o mesmo token do login do portal, com a audiência veltra-api (esquema bearerAuth do contrato). Como esse token vem do login no Keycloak, para a maioria das equipes o caminho mais simples é criar pelo portal e usar a API só para consultar entregas e reenviar.

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 criação de um assinante de Teste:

curl
curl -X POST "https://SEU_ENDERECO_AQUI/v1/integracoes/webhooks" \
  -H "Authorization: Bearer SUA_CHAVE_AQUI" \
  -H "Content-Type: application/json" \
  -d '{
    "nome": "TMS da matriz (teste)",
    "ambiente": "TESTE",
    "url": "https://SEU_RECEPTOR_AQUI/webhooks/veltra",
    "papel": "TMS",
    "tipos": ["baixa.registrada", "ocorrencia.registrada", "comprovante.emitido"],
    "contatoEmail": "integracao@SEU_DOMINIO_AQUI"
  }'

Campos obrigatórios: nome, url, tipos, papel, contatoEmail. ambiente é opcional e, se omitido, vale PRODUCAO. Para TERCEIRO acrescente escopoCnpjs (lista de raízes de 8 caracteres); para posicao.lote de um TMS, cadenciaPosicaoMin (5, 10 ou 15).

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

5. Catálogo de eventos#

São 8 tipos assináveis, todos no mesmo envelope. Além deles existe o webhook.teste, que só o botão de teste envia (seção 9).

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

O envelope (igual para todos)#

CampoTipoDescrição
iduuidIdentificador do evento. Estável. Não é o identificador da tentativa HTTP.
tipotextoUm dos tipos acima.
versaointeiroHoje sempre 1. Campo novo entra sem subir a versão: ignore campos que você não conhece.
ocorridoEmdata e hora (UTC)Momento do fato.
ambienteTESTE ou PRODUCAOAmbiente do evento.
dadosobjetoMuda por tipo (abaixo).

O corpo nunca traz o identificador da transportadora.

parada.chegada e parada.saida#

dados: paradaId (uuid) e manifestoId (uuid), ambos obrigatórios.

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"
  }
}

O parada.saida tem exatamente a mesma forma de dados, com "tipo": "parada.saida".

baixa.registrada#

dados: baixaId, documentoId, paradaId (uuid, obrigatórios) e, opcionais, capturadoEm (hora informada pelo celular, não é o horário legal), recebidoEm (quando o Veltra recebeu), recebedor.nome (só o nome; o documento do recebedor nunca vai no evento), coordenada (latitude, longitude, precisaoM) e quantidadeDeFotos. Se uma baixa cobrir vários documentos, o evento traz o documentoId do primeiro. Terceiro que não é o tomador do documento não recebe recebedor nem coordenada. O contrato prevê ainda pdf e canhoto, que não são preenchidos hoje: para o PDF, espere o comprovante.emitido.

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
  }
}

ocorrencia.registrada#

dados: ocorrenciaId (uuid), documentoId (uuid) e codigo (texto de 1 a 20 caracteres: o código da ocorrência cadastrada na transportadora).

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"
  }
}

comprovante.emitido#

dados: comprovanteId e documentoId (uuid). 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ó com a chave de integração do TMS). O contrato prevê também pdf e canhoto opcionais; terceiro que não é o tomador nunca os recebe.

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"
  }
}

evento_cte.estado_alterado#

dados: documentoId (uuid), codigoEvento (sempre 110180), estado (um de PENDENTE_DE_DADOS, AGUARDANDO_CTE_COMPLETO, AGUARDANDO_REVISAO, PENDENTE_DE_ENVIO, EM_TRANSMISSAO, REGISTRADO, REGISTRADO_COM_ALERTA, REJEITADO, CANCELADO) e, opcionais, protocolo (15 dígitos) e cStat (100 a 999), que aparecem só quando já existem.

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
  }
}

posicao.lote#

dados: placa (7 a 8 caracteres), janelaInicio, janelaFim e pontos (até 60; cada um com lat, lon, precisaoM, registradoEm). É agregado: o Veltra nunca manda um aviso por ponto de GPS e ignora posições simuladas. Só TMS. Com cadência de 10 ou 15 minutos o envio espera o próximo múltiplo, e a janela continua de 5 minutos.

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" }
    ]
  }
}

veiculo.km_registrado#

dados: saidaId (identifica a viagem), momento (SAIDA ou CHEGADA), placa, manifestoIds (lista de uuid), km e, opcionais, kmSaida, kmChegada e kmRodados (kmChegada - kmSaida, só em CHEGADA, quando o km de saída é conhecido). Só TMS. O Veltra não reenvia o histórico anterior à criação deste 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
  }
}

Os exemplos usam identificadores de mentira. Para o detalhe campo a campo (limites e descrições do contrato), veja a seção 3 da referência.

6. Como receber: o que sua rota precisa fazer#

Cada entrega é um POST HTTP/1.1 com Content-Type: application/json e estes cabeçalhos:

CabeçalhoConteúdo
webhook-idIdentificador da entrega: msg_ mais um UUID. Igual em todas as retentativas e no reenvio manual.
webhook-timestampSegundos Unix do momento desta tentativa. Muda a cada tentativa.
webhook-signaturev1, mais o HMAC-SHA256 em Base64. Na rotação do segredo, duas assinaturas separadas por espaço.

Regras de ouro do receptor:

  1. Responda 2xx rápido (o Veltra espera até 5 segundos para conectar e 10 para ler a resposta) e processe depois: grave o evento numa tabela ou fila, devolva 200 ou 204, e trabalhe em segundo plano.
  2. Verifique a assinatura antes de qualquer outra coisa (seção 7), lendo o corpo cru.
  3. Deduplique pelo webhook-id. Repetição é normal (resposta perdida na rede, retentativa, reenvio manual). Insira o webhook-id numa tabela com chave única na mesma transação do processamento; se já existir, responda 2xx sem processar de novo. Guarde por pelo menos 7 dias.
  4. Não conte com a ordem. Uma entrega retentada pode chegar depois de outra mais nova. Compare ocorridoEm para saber qual fato é mais recente e não assuma que baixa.registrada chega antes de comprovante.emitido.
  5. Ignore campos desconhecidos e valide os tipos antes de usar.
  6. Para "tente de novo mais tarde" (banco fora do ar), responda 503, não 400.
Sua respostaO que o Veltra faz
2xxEntregue. Fim.
429, 5xx, sem resposta, demora acima do limite, falha de DNS ou de certificadoFalha temporária: tenta de novo (seção 8). Um Retry-After maior que o próximo intervalo prevalece.
410Entrega esgotada na hora e o webhook é pausado.
3xxNão segue o redirecionamento; entrega esgotada na hora. Cadastre a URL final.
Outro 4xx (400, 401, 403, 404, 422...)Sem nova tentativa: entrega esgotada na hora, vai para a fila de falhas.

7. Verificar a assinatura (HMAC-SHA256)#

O formato é o do Standard Webhooks (ADR 0022). A string assinada, em bytes UTF-8, é:

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

O Veltra assina os bytes que guardou e reenvia exatamente esses bytes em cada tentativa; ele não monta o JSON de novo. Por isso o que vale é o corpo como chegou.

Os passos, na ordem:

Leia o corpo em bytes, sem mexer

Use o corpo cru. Se o framework já fez o parse do JSON e você serializa de novo, a ordem dos campos e os espaços mudam e a assinatura deixa de conferir. Configure leitura "raw" nessa rota.

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. A tolerância de 5 minutos é a recomendação deste manual; o Veltra não a impõe. Mantenha o relógio sincronizado (NTP).

Monte a string assinada

webhook-id, ponto, webhook-timestamp, ponto, corpo.

Obtenha os bytes da chave

O segredo é whsec_ + Base64. Tire o prefixo whsec_ e decodifique o Base64: os bytes resultantes (32) são a chave do HMAC. Erro comum: usar o texto do segredo, com o prefixo, como chave.

Calcule e compare em tempo constante

Calcule o HMAC-SHA256 e compare com cada assinatura que começa com v1, no cabeçalho (separadas por espaço), decodificando a parte Base64. Use a comparação em tempo constante da linguagem; não use ==.

Se nenhuma conferir, recuse

Responda 401 e não processe. Atenção: 401 não é retentado pelo Veltra e conta como entrega esgotada (seção 8). Se o seu segredo estiver errado, cada evento legítimo vai para a fila de falhas.

Os exemplos abaixo seguem exatamente esses passos e o algoritmo de AssinaturaHmac do backend. O segredo é sempre o seu, lido do ambiente (nunca escreva o valor no código).

// 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_..." 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
}

app.listen(3000);

Durante a rotação do segredo (seção 11) o cabeçalho traz duas assinaturas (v1,NOVA v1,ANTIGA); todos os exemplos já percorrem todas as partes v1. Os exemplos não foram executados contra o Veltra real: o algoritmo foi conferido linha a linha com AssinaturaHmac.java.

8. Retentativas, pausa, desativação e fila de falhas#

Intervalos#

A primeira tentativa sai assim que o evento é distribuído (normalmente em segundos). Depois de cada falha temporária:

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

Cada espera varia aleatoriamente de 80% a 120% do valor. Se a resposta trouxer Retry-After maior que o intervalo da tabela, vale o Retry-After; um menor é ignorado.

Quando desiste#

O Veltra desiste quando a próxima tentativa cairia depois de 72 horas da primeira tentativa. A entrega passa a "Tentativas esgotadas" e vai para a fila de falhas (dead letter): as entregas ficam guardadas, não são apagadas, e podem ser reenviadas. Falhas definitivas (410, 3xx, outro 4xx, endereço recusado pela proteção de rede, esquema não permitido) vão para a fila de falhas na hora, sem esperar as 72 horas.

Pausado e desativado#

SituaçãoComo chega láO que acontece
Pausadovocê pausou, ou o endereço respondeu 410Não sai nenhuma entrega nova. Os eventos do período não são apagados. Ao Retomar, voltam a ser distribuídos, sem garantia de ordem.
Desativado automaticamente20 entregas esgotadas seguidas (as 20 últimas concluídas, sem nenhuma entregue no meio; uma entrega bem-sucedida zera a contagem)O Veltra envia e-mail ao contato técnico ("Webhook desativado por falhas seguidas"). Não volta com Retomar: crie um novo webhook e configure o novo segredo no receptor.
Excluídovocê excluiuNa prática vira Desativado: o webhook some da lista. O histórico de entregas já gravado permanece no banco (a tabela não aceita remoção pelo papel da aplicação).

O contrato prevê também a situação COM_FALHAS, que o Veltra ainda não aplica.

Comportamento atual do código: a distribuição considera todo evento do mesmo tipo e ambiente que ainda não tem entrega para o assinante ativo. Na prática, um assinante recém-criado ou retomado pode receber eventos mais antigos do que o cadastro. Por isso a deduplicação e o uso de ocorridoEm (seção 6) valem desde o primeiro dia.

Reenvio manual pelo portal#

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

Reenvio manual 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"

Rota só de ADMIN (JWT do portal, como na seção 4), responde 202; se a entrega não for deste webhook, 404. O reenvio reabre a mesma entrega: mesmo webhook-id e mesmos bytes de corpo, contagem de tentativas zerada, nova janela de 72 horas, e assina de novo com webhook-timestamp novo e o segredo atual. Só sai enquanto o webhook estiver Ativo. O ID_DA_ENTREGA é o "ID da entrega" do histórico no portal (não é o cabeçalho webhook-id).

9. Testar#

O botão Enviar teste#

Na página do webhook, Enviar teste. O Veltra envia ao seu endereço um evento de tipo webhook.teste, assinado como uma entrega real, e mostra o resultado na hora ("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.

JSON
{
  "id": "018f6a2e-7c31-7a3e-8c2a-0000000000ff",
  "tipo": "webhook.teste",
  "versao": 1,
  "ocorridoEm": "2026-10-22T15:00:00Z",
  "ambiente": "TESTE",
  "dados": { "mensagem": "Evento de teste enviado pelo portal do Veltra." }
}
  • O evento de teste sempre leva "ambiente": "TESTE", mesmo num webhook de Produção; 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. Um webhook pausado também pode ser testado.
  • As regras de segurança do endereço valem iguais. Motivos que o teste pode devolver incluem ENDERECO_RECUSADO, ESQUEMA_RECUSADO, DNS, CONEXAO, COFRE_INDISPONIVEL ou HTTP_<código> (a resposta do seu endereço não foi 2xx).
  • Não existe, hoje, botão para disparar um exemplo de cada evento de negócio. O botão envia só o webhook.teste. Para ver baixa.registrada e os demais, gere os fatos no ambiente de Teste do Veltra e acompanhe o histórico de entregas.

Desenvolver na sua máquina#

O servidor do Veltra só alcança endereços públicos: o localhost do seu notebook não é visível para ele. Use um túnel que dê a você um endereço https público apontando para a sua máquina (opções gratuitas: Cloudflare Tunnel com cloudflared, ngrok, localtunnel). Rode o receptor na porta 3000, abra o túnel para essa porta e cadastre o endereço https que a ferramenta mostrar. O endereço do túnel é público: deixe aberto só enquanto testa, e verifique a assinatura mesmo assim.

Testar o receptor sem o Veltra#

Você pode assinar uma chamada e disparar contra o seu receptor local, com openssl e curl:

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

# 2. Corpo, id e 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. Mude um caractere de corpo.json depois de assinar, ou espere mais de 5 minutos, e deve responder 401. Rode o mesmo envio duas vezes e confira que o segundo é reconhecido como repetido.

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

  1. O botão Enviar teste voltou 2xx.
  2. Corpo alterado é recusado com 401.
  3. webhook-timestamp antigo é recusado.
  4. webhook-id repetido não é processado duas vezes.
  5. O receptor responde em poucos segundos mesmo com o banco lento.
  6. A troca de segredo foi ensaiada (seção 11).
  7. Foi criado um webhook novo, de Produção, com https:// (o de Teste não migra de ambiente).

10. Problemas comuns#

Checklist de diagnóstico#

Olhe primeiro o Histórico de entregas do webhook (Situação, Código HTTP, Tentativas, Duração) e siga:

  1. Código 401 em tudo → assinatura. Confira: segredo do webhook certo; prefixo whsec_ tirado e Base64 decodificado; corpo lido cru; string id.timestamp.corpo; cabeçalhos lidos sem diferenciar maiúsculas.
  2. Sem código HTTP e esgotada na hora → o Veltra recusou o destino: http em Produção, endereço de rede interna, esquema não permitido ou redirecionamento. Cadastre https público e final.
  3. Sem código HTTP e com várias tentativas → timeout (acima de 10 s), DNS, conexão ou certificado TLS inválido. Responda rápido; confira DNS e a cadeia do certificado (autoridade pública, nome do servidor, não vencido).
  4. 404 ou 400 → rota errada ou validação do seu lado. Não é retentado: corrija e use Reenviar.
  5. 5xx ou 429 → o Veltra retenta sozinho (seção 8); corrija e espere a próxima tentativa.
  6. Nada chega e o webhook está Ativo → confira os tipos marcados, o ambiente (Teste e Produção são separados) e se o evento já é gerado naquele ambiente.
SintomaCausa provávelCorreção
401 só em alguns eventosParse e nova serialização do JSON antes de verificarLeia o corpo em bytes na rota do webhook
Funcionava, agora recusa tudo por tempoRelógio do seu servidor fora por mais de 5 minutosSincronize (NTP)
Falha logo depois de girar o segredoReceptor com o segredo antigo depois das 24 horas, ou duas rotaçõesUse o segredo mais recente; se perdeu, gire de novo
Webhook "Pausado" sem você ter pausadoO endereço respondeu 410Corrija o endereço e clique em Retomar
"Desativado automaticamente"20 entregas esgotadas seguidasCorrija a causa e crie um webhook novo
Teste voltou ENDERECO_RECUSADOEndereço aponta para rede internaUse um endereço público ou um túnel
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
Quero o PDF e ele não vemO webhook leva só o comprovanteIdBaixe pela API de leitura com a chave de integração

Perguntas frequentes#

Perdi o segredo. Dá para ver de novo? Não. Ele aparece uma única vez. Use Girar segredo.

Posso mudar um webhook de Teste para Produção? Não. O ambiente não muda. Crie outro.

O mesmo evento chegou duas vezes. É defeito? Não. A entrega é "pelo menos uma vez". Deduplique pelo webhook-id.

Os eventos chegam fora de ordem. É esperado. Ordene por ocorridoEm.

Meu servidor ficou fora do ar por mais de 72 horas e perdi eventos. Recupere pela API de leitura com alteradoDesde (veja a referência) ou use o reenvio da fila de falhas.

Existe uma lista de IPs de origem para liberar no firewall? Não há lista publicada. A prova de autenticidade é a assinatura; se sua política de rede exige liberar IPs, combine com a verificação, nunca no lugar dela.

11. Segurança e rotação de segredo#

  • Verifique sempre a assinatura e o relógio. Sem isso, qualquer pessoa que descubra o endereço envia eventos falsos.
  • Guarde o segredo em cofre. Nunca em repositório, código, chat ou log. Registre apenas o webhook-id e o id do evento, jamais o segredo nem a assinatura completa.
  • Não dependa de IP de origem. Não há lista de IPs; a assinatura é a prova.
  • Limite o tamanho do corpo aceito (os eventos são pequenos; 1 MB sobra) e trate o conteúdo como dado: valide tipos e ignore campos desconhecidos.
  • Dados pessoais. Motoristas, destinatários e localização são dados pessoais: trate-os conforme a LGPD e não os grave em logs abertos.
  • HTTPS em Produção. O Veltra só entrega em https nos webhooks de Produção, não segue redirecionamentos, valida o IP de cada entrega (bloqueia rede interna, link-local, CGNAT e multicast, e loopback fora do Teste) e liga a conexão direto no IP validado, para que um endereço cadastrado não faça o Veltra chamar uma rede interna.

Girar o segredo sem perder eventos#

Gire

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

Janela de 24 horas

Por 24 horas, cada entrega leva duas assinaturas em webhook-signature, separadas por espaço: primeiro a do segredo novo, depois a do antigo. Os receptores da seção 7 já conferem todas.

Troque no receptor

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

Depois de 24 horas

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

Se girar duas vezes dentro de 24 horas, o "anterior" passa a ser o do penúltimo giro e o primeiro deixa de valer na hora. Se o segredo vazou, um giro deixa o vazado valendo por mais 24 horas; para cortá-lo já, gire duas vezes seguidas e atualize o receptor com o último segredo logo em seguida. Editar o webhook (nome, URL, tipos) não gira o segredo.

12. Resumo#

  • Webhook é um POST do Veltra para o seu endereço. Responda 2xx rápido e 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 por até 72 horas (1 min, 5 min, 30 min, 2 h, 12 h, 12 h...). Outros 4xx e 3xx não; 410 pausa o webhook.
  • Duplicata é normal (deduplique por webhook-id); ordem não é garantida.
  • Segredo aparece uma vez; giro tem 24 horas com duas assinaturas.
  • Rede de segurança: consulta incremental com alteradoDesde.