Receber eventos
Um webhook avisa o seu servidor na hora em que algo acontece na loja: a Zolvi faz um POST assinado na sua URL, com o evento e o objeto em JSON.
Configurar
- No painel, abra Integrações › Webhooks e clique em Novo webhook (ou use a API).
- Informe a URL (https e endereço público) e escolha os eventos.
- Guarde o segredo (
whsec_…): é com ele que você confere que o envio veio da Zolvi. - Use Enviar teste para receber um evento
PINGe conferir.
Eventos
| Evento | Quando | data |
|---|---|---|
ORDER_CREATED | Pedido criado (o cliente gerou o Pix) | O pedido, com o pix_copy_paste preenchido. |
ORDER_PAID | Pedido pago | O pedido (status paid). |
ORDER_DELIVERED | Pedido entregue ao cliente | O pedido (status delivered). |
ORDER_EXPIRED | Pix do pedido venceu sem pagamento | O pedido (status expired). |
ORDER_CANCELLED | Pedido cancelado | O pedido (status cancelled). |
PRODUCT_CREATED | Produto criado | { product } |
PRODUCT_UPDATED | Produto alterado | { before, after }: o produto antes e depois da mudança. |
PRODUCT_DELETED | Produto apagado | { product }: como estava antes de apagar. |
STOCK_UPDATED | Estoque adicionado ou removido | { product_id, product_name, variant_id, variant_name, added, removed, stock_available } |
STOCK_LOW | Estoque chegou ao limite de aviso ou esgotou | { product_id, product_name, variant_id, variant_name, stock_available, sold_out, threshold } |
REVIEW_CREATED | Cliente avaliou um pedido | A avaliação: { order_id, order_number, rating, comment, customer, products }. |
AFFILIATE_SALE | Venda com código de apoiador de um afiliado | { affiliate, commission_cents, order } |
Pedidos usam o mesmo formato de GET /orders/{number} e produtos o de GET /products/{id}. Uma venda normal dispara ORDER_CREATED (Pix gerado), ORDER_PAID e ORDER_DELIVERED, nessa ordem.
Formato
POST na sua URL
{
"id": "evt_8ZcZT72Gy-jnD1xK5GTXcKIKgZQ",
"event": "ORDER_PAID",
"date": "2026-11-08T15:31:12.000Z",
"data": {
"id": "cmw8d6urw0001ssugmol9xqu2",
"number": 1042,
"status": "paid",
"customer": {
"id": "cmvrlovla0001ssvcuio6xvf7",
"discord_id": "1390611228354809977",
"username": "cliente"
},
"items": [
{
"id": "cmvrlovlp0005ssvc3yv6nmtz",
"product_id": "cmvrllzcq000bssokqrrb39n8",
"product_name": "Key Steam",
"variant_id": "cmvrllzcq000cssokzx1yq2pa",
"variant_name": "30D",
"quantity": 2,
"unit_price_cents": 1500,
"total_cents": 3000,
"answer": null
}
],
"subtotal_cents": 3000,
"discount_cents": 300,
"total_cents": 2700,
"coupon_code": "BLACK10",
"affiliate": null,
"payment": {
"provider": "mercadopago",
"method": "gateway",
"verified": true,
"approved_by": null,
"id": "123456789",
"pix_copy_paste": null,
"created_at": "2026-11-08T15:30:00.000Z",
"expires_at": "2026-11-08T16:00:00.000Z",
"paid_at": "2026-11-08T15:31:12.000Z",
"gateway_fee_cents": 27
},
"delivery": {
"delivered_at": null,
"manual": false,
"manual_reason": null
},
"review": null,
"recovered_from_number": null,
"created_at": "2026-11-08T15:29:40.000Z",
"updated_at": "2026-11-08T15:31:12.000Z"
}
}| Cabeçalho | O que é |
|---|---|
x-zolvi-signature | HMAC-SHA256 em hexadecimal de “timestamp.corpo”, com o segredo do webhook. |
x-zolvi-timestamp | Horário do envio, em segundos (Unix). |
x-zolvi-event | O tipo do evento (igual ao campo event). |
x-zolvi-event-id | O id do evento (igual ao campo id): use para ignorar repetidos. |
Conferir a assinatura
Calcule HMAC-SHA256(segredo, timestamp + "." + corpo) sobre os bytes exatos do corpo recebido e compare com o x-zolvi-signature numa comparação de tempo constante. Recuse também envios com o horário mais de 5 minutos longe do seu relógio.
Node.js
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.ZOLVI_WEBHOOK_SECRET; // whsec_…
// Use o corpo cru (os bytes exatos), não o JSON já convertido.
app.post("/webhooks/zolvi", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.get("x-zolvi-signature") ?? "";
const timestamp = req.get("x-zolvi-timestamp") ?? "";
const body = req.body.toString("utf8");
// Recusa envio com mais de 5 minutos (contra reenvio de mensagem velha).
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(400);
const expected = crypto.createHmac("sha256", SECRET).update(`${timestamp}.${body}`).digest("hex");
const ok = signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(body);
res.sendStatus(200); // responda logo; processe depois
if (event.event === "ORDER_PAID") {
// libere o VIP, mande e-mail etc. Use event.id para ignorar repetidos.
}
});
app.listen(3000);Não converta o JSON e monte de novo antes de conferir: espaços e ordem dos campos mudam os bytes e a assinatura deixa de bater. Use o corpo cru.
Respostas e novas tentativas
- Responda com 2xx em até 10 segundos. Qualquer outro código, ou demora, conta como falha. Faça o trabalho pesado depois de responder.
- Falhou, a Zolvi tenta de novo em 1 min, 5 min, 30 min, 2 h, 6 h, 12 h e 24 h (8 tentativas, cerca de 1 dia e meio).
- O mesmo evento pode chegar mais de uma vez (por exemplo, se a sua resposta se perder): use o
idpara não processar duas vezes. - A ordem não é garantida entre eventos diferentes: confira o status do objeto, não só o tipo do evento.
- Webhook que só falha há 3 dias é desligado sozinho e a loja é avisada no Registro. Ligue de novo no painel.
- Redirecionamentos não são seguidos, e endereços de rede interna são recusados.
- As entregas ficam 30 dias no painel, com o código de resposta, e dá para reenviar.
Exemplo: PRODUCT_UPDATED
Corpo
{
"id": "evt_Qm1x…",
"event": "PRODUCT_UPDATED",
"date": "2026-11-08T16:02:00.000Z",
"data": {
"before": {
"id": "cmvrllzcq000bssokqrrb39n8",
"name": "Key Steam",
"description": "Key de ativação na Steam.",
"image_url": "https://exemplo.com/key.png",
"active": true,
"published": true,
"role_id": null,
"role_days": null,
"allowed_role_ids": [],
"min_quantity": null,
"max_quantity": 5,
"hide_stock": false,
"question": null,
"delivery_note": "Resgate em store.steampowered.com/account/redeemwalletcode",
"variants": [
{
"id": "cmvrllzcq000cssokzx1yq2pa",
"name": "30D",
"price_cents": 1500,
"compare_at_cents": 1990,
"cost_cents": 500,
"active": true,
"position": 0,
"fixed_delivery": false,
"stock_available": 42
}
],
"created_at": "2026-10-28T04:10:00.000Z",
"updated_at": "2026-11-08T15:31:12.000Z"
},
"after": {
"id": "cmvrllzcq000bssokqrrb39n8",
"name": "Key Steam",
"description": "Key de ativação na Steam.",
"image_url": "https://exemplo.com/key.png",
"active": false,
"published": true,
"role_id": null,
"role_days": null,
"allowed_role_ids": [],
"min_quantity": null,
"max_quantity": 5,
"hide_stock": false,
"question": null,
"delivery_note": "Resgate em store.steampowered.com/account/redeemwalletcode",
"variants": [
{
"id": "cmvrllzcq000cssokzx1yq2pa",
"name": "30D",
"price_cents": 1500,
"compare_at_cents": 1990,
"cost_cents": 500,
"active": true,
"position": 0,
"fixed_delivery": false,
"stock_available": 42
}
],
"created_at": "2026-10-28T04:10:00.000Z",
"updated_at": "2026-11-08T15:31:12.000Z"
}
}
}