API Admin › Guias
Webhooks
Um webhook avisa o sistema da integração na hora em que algo muda na loja: o pedido feito no checkout, o pagamento confirmado, o estoque que mudou, o tema publicado. É um POST assinado para um endereço https da integração, com o recurso como está no momento do envio. Assim o ERP não precisa perguntar à API a cada minuto se algo mudou.
Os webhooks são de uma integração. Eles são cadastrados pelo painel (Configurações › Integrações › a integração › Webhooks) ou pela própria integração, pela API (/webhooks). Cada integração tem até 10.
Os eventos
| Evento | Quando | data | Escopo |
|---|---|---|---|
order.created | o pedido feito no checkout | o pedido | read_orders |
order.paid | o pagamento confirmado (pelo gateway, pelo painel ou por mark_paid) | o pedido | read_orders |
order.updated | em separação, o rastreio que veio da etiqueta, a nota fiscal informada, o pedido que entrou numa conta ou saiu dela | o pedido | read_orders |
order.shipped | o pedido enviado (à mão ou pela etiqueta postada) | o pedido | read_orders |
order.delivered | o pedido entregue | o pedido | read_orders |
order.cancelled | o pedido cancelado (pela loja, pela API ou porque não foi pago no prazo) | o pedido | read_orders |
order.anonymized | os dados de quem comprou apagados (LGPD): apague também a sua cópia | o pedido, já anonimizado | read_orders |
order.prescription_received | a receita pendente de um item chegou depois do pedido (painel ou API) | só { "id", "number", "line_item_id" }: a receita sai no GET /orders/{id} | read_orders |
product.created | o produto criado (painel, planilha, API ou cópia) | o produto | read_products |
product.updated | o produto alterado: os campos, as variantes, os preços, as fotos, a edição em massa, a coleção ou a marca dele excluída | o produto | read_products |
product.deleted | o produto excluído | só { "id" } | read_products |
inventory.updated | o estoque de uma variante mudou (checkout, cancelamento, painel, planilha, API) | a variante | read_products |
theme.published | um tema foi ao ar (painel, API, agendamento, volta a uma publicação ou push no GitHub com Publicar sozinho) | o tema | read_themes |
customer.created | o comprador entrou pela primeira vez na área do cliente (a conta nasceu) | o cliente | read_customers |
customer.updated | os dados, o e-mail, as novidades ou os endereços da conta mudaram (na área do cliente ou no checkout) | o cliente | read_customers |
customer.deleted | a conta foi excluída pelo comprador ou em Privacidade: apague também a sua cópia | só { "id" } | read_customers |
Assinar um evento pede o escopo de leitura do que ele avisa (sem ele, 403 missing_scope). Se o escopo for tirado depois, as entregas daquele evento param.
O que chega
POST /webhooks/denaryx HTTP/1.1
Content-Type: application/json
User-Agent: Denaryx/1.0 (+https://denaryx.com)
Denaryx-Signature: t=1759600000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Denaryx-Event-Id: 2f6c0a8e-...
Denaryx-Event-Type: order.paid
Denaryx-Delivery-Id: 9b1e44d0-...
Denaryx-Api-Version: 2026-10
{
"id": "2f6c0a8e-...",
"type": "order.paid",
"created_at": "2026-10-04T13:20:00.000Z",
"api_version": "2026-10",
"data": { "id": "6c1d0a52-...", "number": 1021, "status": "paid", ... }
}idé o id do evento: o mesmo em todas as tentativas e no reenvio. Use para não processar o mesmo evento duas vezes.created_até quando a mudança aconteceu.dataé o recurso como está na hora do envio, no formato da API, e não como estava no momento do evento. Umorder.createdque sai depois de o pedido ser pago já traz"status": "paid". Na exclusão, e quando o recurso não existe mais,dataé só{ "id" }.- A ordem de chegada não é garantida. Dois eventos do mesmo pedido podem chegar trocados, e uma nova tentativa chega depois de eventos mais novos. Decida pelo
data(que é o de agora) ou peloupdated_atdele, e não pela ordem. - O mesmo evento pode chegar mais de uma vez (uma resposta que se perdeu na rede, um reenvio). Trate a entrega como "pelo menos uma vez".
A assinatura
Toda entrega leva Denaryx-Signature: t=<segundos>,v1=<assinatura>. A assinatura é o HMAC-SHA256, em hexadecimal, de <t>.<corpo> com o segredo do webhook. O segredo (dnxw_...) aparece uma vez só, na criação e na troca.
Para conferir:
- Leia o corpo cru, exatamente como chegou (antes de interpretar o JSON).
- Separe o
te osv1do cabeçalho. - Calcule
HMAC-SHA256(segredo, t + "." + corpo)e compare com cadav1, numa comparação de tempo constante. Basta um conferir. - Recuse se o
testiver a mais de 5 minutos do seu relógio: é alguém repetindo uma entrega antiga.
import { createHmac, timingSafeEqual } from 'node:crypto';
function assinaturaConfere(cabecalho, segredo, corpo) {
const partes = cabecalho.split(',').map((parte) => parte.split('=', 2));
const t = Number(partes.find(([chave]) => chave === 't')?.[1]);
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300)
return false;
const esperado = createHmac('sha256', segredo)
.update(`${t}.${corpo}`)
.digest();
return partes.some(
([chave, valor]) =>
chave === 'v1' &&
/^[0-9a-f]{64}$/.test(valor) &&
timingSafeEqual(Buffer.from(valor, 'hex'), esperado),
);
}Troca do segredo: gere outro no painel ou com PATCH /webhooks/{id} e "rotate_secret": true. Por 24 horas, as entregas levam dois v1, um com o segredo novo e outro com o anterior: troque o segredo no seu sistema nesse intervalo, sem perder entrega.
Respostas, novas tentativas e desligamento
- Responda 2xx em até 10 segundos. Qualquer outra coisa é falha: 3xx (o redirecionamento não é seguido), 4xx, 5xx, a conexão recusada, o certificado inválido, o prazo. O corpo da resposta não é lido.
- Faça o mínimo antes de responder: guarde o evento e processe depois, numa fila sua.
- A entrega que falha é tentada de novo, até 10 vezes, com espera crescente: 1 minuto, 5 minutos, 15 minutos, 1 hora, 2, 4, 6, 8 e 14 horas, cerca de 36 horas ao todo.
- Com 3 dias seguidos sem nenhuma entrega dar certo, o webhook é desligado (
status: "disabled"), as entregas que esperavam nova tentativa param, e quem cuida das integrações da loja recebe um e-mail. Para voltar: confira o servidor, mande um teste, reative (PATCHcom"status": "active"ou o botão no painel) e reenvie pelo painel as entregas que falharam. - Com a loja suspensa pela Denaryx, as entregas esperam: nada sai, e nenhuma tentativa conta. Na reativação, as que esperaram saem logo. Os eventos ficam 30 dias: uma suspensão mais longa que isso perde os mais antigos.
- O endereço precisa ser
https, na porta 443, de um servidor da internet: endereços de rede interna são recusados, no cadastro e a cada entrega.
Teste e reenvio
POST /webhooks/{id}/test(ou "Enviar teste" no painel) manda na hora o eventowebhook.test, assinado, numa tentativa só, e responde se o endereço deu 2xx. Vale também para o webhook pausado ou desligado, para conferir antes de reativar. O teste não conta para o desligamento.- No painel, cada entrega mostra a situação, o código da resposta, o erro e a duração, e tem o botão Reenviar: o mesmo evento sai de novo, numa entrega nova, com as tentativas do começo.
Perdeu uma entrega?
Os eventos ficam 30 dias em GET /events, na ordem em que aconteceram. Depois de um tempo fora do ar, leia os eventos desde a última entrega que você processou e busque os recursos que mudaram. Para uma sincronização completa, use as listas com updated_at_min (Listas e sincronização).
As rotas
Qualquer token da integração cuida dos webhooks dela; os de outras integrações da loja não aparecem. O login do CLI pela conta não cuida de webhooks (403 cli_session_not_allowed).
| Rota | O que faz |
|---|---|
GET /webhooks | os webhooks da integração (sem o segredo) |
POST /webhooks | cadastra { "url", "events" }; a resposta traz o secret |
GET /webhooks/{id} | um webhook |
PATCH /webhooks/{id} | muda url, events ou status (active, paused); com "rotate_secret": true, troca o segredo e o devolve em secret |
DELETE /webhooks/{id} | exclui; as entregas pendentes não saem mais |
POST /webhooks/{id}/test | o envio de teste |
curl -X POST https://api.denaryx.com/admin/2026-10/webhooks \
-H "Authorization: Bearer dnxa_..." \
-H "Content-Type: application/json" \
-d '{"url": "https://erp.exemplo.com.br/webhooks/denaryx", "events": ["order.paid", "inventory.updated"]}'{
"id": "4a0b...",
"url": "https://erp.exemplo.com.br/webhooks/denaryx",
"events": ["order.paid", "inventory.updated"],
"status": "active",
"failing_since": null,
"created_at": "2026-10-04T13:00:00.000Z",
"updated_at": "2026-10-04T13:00:00.000Z",
"secret": "dnxw_..."
}Com Idempotency-Key, a repetição da criação devolve o mesmo webhook, mas sem o secret: ele não fica guardado em lugar nenhum além da cifra do próprio webhook. Se perder a primeira resposta, troque o segredo.
Erros próprios: 400 invalid_url (endereço que não é https público), 403 missing_scope (um evento sem o escopo de leitura), 409 webhook_limit_reached (já há 10).
Dados pessoais
Nenhuma entrega guarda o corpo: ele é montado a cada tentativa, com o recurso de agora. O pedido anonimizado sai anonimizado, também no reenvio. Os eventos de GET /events guardam só um resumo sem dado pessoal (o número e a situação do pedido, o estoque da variante).