Pular para o conteúdo principal

Webhook de venda

A ETech envia ao backend do ERP um POST assinado a cada evento de venda: concluída, estornada, com NFC-e emitida ou com a nota pedida por e-mail. O feed devolve os mesmos eventos, na mesma ordem, para reconciliar o que não chegou.

O que o cliente cadastra​

O administrador da organização cadastra o webhook no painel da ETech, em Integrações → Webhook de venda.

CampoRegra
URLhttps://, porta 443, com nome que resolve para IP público. Endereço privado, loopback ou link-local é recusado na gravação e de novo a cada envio
Header de autenticaçãoOpcional: o nome e o valor que o backend do ERP exige, como X-Api-Key. Não pode ser um header de transporte (Content-Type, User-Agent, Host, Content-Length e afins) nem começar com X-Etech-
E-mail técnicoObrigatório. Recebe o aviso quando o envio é suspenso
EventosOs que o cliente quer receber. Nenhum marcado assina todos
Segredo de assinaturaGerado no painel e mostrado uma única vez. Sem ele, nenhum evento é enviado
  • O segredo não pode ser recuperado, só trocado. A troca vale já no envio seguinte.
  • Redirect não é seguido: uma resposta 3xx conta como falha.

Os eventos​

Geram evento as vendas de máquina de vending e, quando o ERP externo emite a NFC-e, as de qualquer canal (veja NFC-e pelo ERP externo). Cada evento leva a venda inteira como está naquele momento.

typeQuando nasce
sale.completedVenda paga e concluída
sale.canceledVenda estornada
sale.invoice_issuedNFC-e autorizada, quando a ETech emite a nota
sale.receipt_requestedConsumidor pediu a NFC-e por e-mail, quando o ERP externo emite a nota
integration.pingBotão "Testar" do painel. Sempre enviado, mesmo que não esteja entre os eventos marcados

Uma venda pode gerar mais de um evento, e eles podem chegar fora de ordem. O sequence resolve isso (veja Responder e reentregas).

O POST​

Cada evento é um POST para a URL cadastrada, com o corpo em JSON e estes headers:

POST <URL cadastrada>
Content-Type: application/json
User-Agent: etech-webhook/1
X-Etech-Event-Id: 0f7c2d1e-4b9a-4c3e-8d21-6a5b7c8d9e01
X-Etech-Timestamp: 1792681200
X-Etech-Signature: v1=<64 caracteres hexadecimais>
X-Api-Key: <valor cadastrado, só se houver header de autenticação>
  • O id do corpo e o X-Etech-Event-Id são o mesmo valor.
  • O corpo de uma reentrega é idêntico ao da primeira tentativa, byte a byte. Só o timestamp e a assinatura mudam.
  • No integration.ping, o data é { "message": "ping" }.

O payload v1​

Valores em reais, com ponto decimal; datas em ISO 8601 com fuso. Campos novos podem ser acrescentados na mesma versão: ignore os que não conhecer.

{
"id": "0f7c2d1e-4b9a-4c3e-8d21-6a5b7c8d9e01",
"type": "sale.completed",
"sequence": 184223,
"occurredAt": "2026-10-22T15:00:00.412+00:00",
"organizationId": 2,
"data": {
"saleId": 98765,
"uuid": "3b0e8c52-6f1a-4d7e-9a43-2c5d8e1f7b60",
"status": "Completed",
"channel": "VendingMachine",
"completedAt": "2026-10-22T14:59:58.12+00:00",
"total": 21.8,
"discount": 0,
"company": { "document": "12345678000190" },
"store": { "id": 12, "name": "Loja Exemplo" },
"payBox": { "id": 34, "serial": "A4:CF:12:9B:3E:01" },
"customer": { "document": "12345678909" },
"items": [
{ "skuId": 555, "ean": "7891234567895", "description": "Cerveja IPA 473 ml",
"quantity": 2, "unitPrice": 10.9, "discount": 0, "total": 21.8 }
],
"payments": [
{ "method": "Crédito", "amount": 21.8, "brand": "VISA", "nsu": "000123456", "authorizationCode": "A1B2C3",
"type": "CreditCard", "interface": "Tef", "acquirerDocument": "10440482000154" }
],
"invoice": {
"issuer": "Etech", "email": null,
"number": null, "serie": null, "key": null, "status": null,
"externalKey": null, "externalUrl": null
}
}
}
CampoTipoO que é
idGUIDIdentificador do evento. Chave de deduplicação
typetextoUm dos cinco eventos
sequenceinteiroCresce a cada evento da organização. É o cursor do feed
occurredAtdata e horaQuando o evento nasceu
organizationIdinteiroA organização do cliente na ETech
data.saleIdinteiroA venda na ETech. É o id de GET /integration/v1/sales/{id}
data.uuidtextoIdentificador da venda gerado pela máquina
data.statustextoCompleted ou Canceled
data.channeltextoO canal da venda na ETech. A máquina de vending vem como VendingMachine; os outros canais só chegam com a NFC-e emitida pelo ERP externo
data.completedAtdata e horaQuando a venda foi concluída
data.totalnúmeroValor líquido da venda
data.discountnúmero ou nuloDesconto da venda
data.company.documenttextoCNPJ da empresa da loja, só dígitos
data.storeobjetoid e name da loja
data.payBoxobjetoid e serial da máquina. O serial é o MAC
data.customerobjeto ou nulodocument (CPF) do consumidor. Nulo quando ele não se identificou
data.items[]listaskuId, ean, description, quantity, unitPrice, discount e total de cada item
data.payments[]listamethod, amount, brand, nsu, authorizationCode, type, interface e acquirerDocument de cada pagamento não cancelado. Os três últimos estão detalhados em NFC-e pelo ERP externo
data.invoiceobjetoissuer (quem emite a NFC-e: Etech, ExternalErp ou None), email (o pedido da nota por e-mail), number, serie, key (chave de acesso) e status da nota emitida pela ETech, e externalKey e externalUrl da nota que o ERP externo devolveu. Sempre presente; os campos da nota ficam nulos antes da emissão
data.loyaltyobjeto ou nuloO uso do saldo do programa de fidelidade, quando a venda usou. Detalhado em Benefícios do consumidor

Com a nota emitida pela ETech, ela costuma chegar depois da venda: o sale.completed vem com invoice.key nulo, e o sale.invoice_issued traz a venda com a nota preenchida.

Conferir a assinatura​

A assinatura prova que o POST veio da ETech e que o corpo não mudou. O backend do ERP confere antes de processar:

  1. Recuse o X-Etech-Timestamp (segundos Unix) que estiver a mais de 5 minutos do relógio do servidor.
  2. Monte a mensagem: o timestamp, um ponto e o corpo cru, exatamente como chegou, antes de qualquer parse.
  3. Calcule o HMAC-SHA256 da mensagem. A chave são os bytes UTF-8 do segredo como texto; não decodifique o base64url.
  4. Compare v1= + o hexadecimal minúsculo com o X-Etech-Signature, em tempo constante.

Assinatura inválida ou timestamp fora da janela: responda 401 e não processe. A ETech trata como falha e tenta de novo.

O exemplo de assinatura traz o código em C#, Node.js, Python e Java, e um vetor de teste para conferir a sua implementação.

Responder e reentregas​

Um 2xx em até 10 segundos conta como entregue. Qualquer outra resposta, timeout, erro de rede ou 3xx conta como falha, e o evento é reenviado.

  • Responda rápido: grave o evento e processe depois. Os 10 segundos incluem a conexão.
  • O mesmo evento pode chegar mais de uma vez. Deduplique pelo id e responda 2xx ao repetido, sem efeito novo.
  • A ordem de chegada não é garantida. Descarte o evento cujo sequence for menor que o último que você aplicou para a mesma venda (data.saleId).
  • Intervalos entre tentativas: 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h e depois a cada 8 h, até 72 h da criação do evento. Esgotado o prazo, o evento fica como falho. O integration.ping tem uma tentativa só.
  • Suspensão: 24 h sem nenhum 2xx suspendem o envio, e o e-mail técnico recebe o aviso. Os eventos continuam sendo gravados. Ao reativar no painel, os pendentes saem em ordem de criação.
  • Reenvio pelo painel: um evento entregue, falho ou ignorado pode ser reenviado, com o mesmo id e o mesmo corpo.

O exemplo de receptor junta assinatura, deduplicação e descarte por sequence.

O feed e a consulta de venda​

O feed devolve os eventos da organização em ordem de sequence, cada um com o mesmo corpo do POST. Serve para recuperar o que não chegou, inclusive durante uma suspensão. As chamadas usam o token e os headers de Acesso.

GET /integration/v1/events?after=184200&limit=100
x-api-key: <api key>
Authorization: Bearer <access_token>
User-Agent: <nome-do-sistema>/<versão>
{ "events": [ { "id": "0f7c2d1e-…", "type": "sale.completed", "sequence": 184223, "…": "…" } ], "next": 184223 }
  • after é o último sequence aplicado. Ausente ou zero, começa do evento mais antigo guardado.
  • limit vai de 1 a 500, com padrão 100.
  • Guarde o next e use como after da próxima chamada. Uma página vazia devolve o mesmo after.
  • Só entram eventos com mais de 5 segundos, para que nenhum fique para trás do cursor.
  • O feed alcança os últimos 30 dias.

A consulta de venda devolve a venda como está agora, no mesmo formato do data:

GET /integration/v1/sales/98765

Venda de outra organização, ou que o webhook não leva, responde 404 com INTEGRATION.SALE_NOT_FOUND. Os outros erros estão em Acesso.

O exemplo de feed mostra a leitura com cursor.