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.
| Campo | Regra |
|---|---|
| URL | https://, 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ção | Opcional: 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écnico | Obrigatório. Recebe o aviso quando o envio é suspenso |
| Eventos | Os que o cliente quer receber. Nenhum marcado assina todos |
| Segredo de assinatura | Gerado 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
3xxconta 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.
type | Quando nasce |
|---|---|
sale.completed | Venda paga e concluída |
sale.canceled | Venda estornada |
sale.invoice_issued | NFC-e autorizada, quando a ETech emite a nota |
sale.receipt_requested | Consumidor pediu a NFC-e por e-mail, quando o ERP externo emite a nota |
integration.ping | Botã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
iddo corpo e oX-Etech-Event-Idsã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, odataé{ "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
}
}
}
| Campo | Tipo | O que é |
|---|---|---|
id | GUID | Identificador do evento. Chave de deduplicação |
type | texto | Um dos cinco eventos |
sequence | inteiro | Cresce a cada evento da organização. É o cursor do feed |
occurredAt | data e hora | Quando o evento nasceu |
organizationId | inteiro | A organização do cliente na ETech |
data.saleId | inteiro | A venda na ETech. É o id de GET /integration/v1/sales/{id} |
data.uuid | texto | Identificador da venda gerado pela máquina |
data.status | texto | Completed ou Canceled |
data.channel | texto | O 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.completedAt | data e hora | Quando a venda foi concluída |
data.total | número | Valor líquido da venda |
data.discount | número ou nulo | Desconto da venda |
data.company.document | texto | CNPJ da empresa da loja, só dígitos |
data.store | objeto | id e name da loja |
data.payBox | objeto | id e serial da máquina. O serial é o MAC |
data.customer | objeto ou nulo | document (CPF) do consumidor. Nulo quando ele não se identificou |
data.items[] | lista | skuId, ean, description, quantity, unitPrice, discount e total de cada item |
data.payments[] | lista | method, 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.invoice | objeto | issuer (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.loyalty | objeto ou nulo | O 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:
- Recuse o
X-Etech-Timestamp(segundos Unix) que estiver a mais de 5 minutos do relógio do servidor. - Monte a mensagem: o timestamp, um ponto e o corpo cru, exatamente como chegou, antes de qualquer parse.
- Calcule o HMAC-SHA256 da mensagem. A chave são os bytes UTF-8 do segredo como texto; não decodifique o base64url.
- Compare
v1=+ o hexadecimal minúsculo com oX-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
ide responda2xxao repetido, sem efeito novo. - A ordem de chegada não é garantida. Descarte o evento cujo
sequencefor 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.pingtem uma tentativa só. - Suspensão: 24 h sem nenhum
2xxsuspendem 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
ide 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 últimosequenceaplicado. Ausente ou zero, começa do evento mais antigo guardado.limitvai de 1 a 500, com padrão 100.- Guarde o
nexte use comoafterda próxima chamada. Uma página vazia devolve o mesmoafter. - 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.