Desenvolvedores
Webhooks
Eventos, assinatura HMAC e regras de entrega do webhook próprio.
O webhook próprio envia um POST em JSON para o seu sistema quando algo acontece na loja. Para cadastrar, veja o guia Webhook próprio. Esta página é a referência técnica.
A requisição#
| Cabeçalho | Conteúdo |
|---|---|
content-type |
application/json |
user-agent |
PromisseStore-Webhook/1.0 |
x-webhook-signature |
HMAC-SHA256 em hexadecimal do corpo bruto, com o Secret como chave |
x-webhook-event |
Nome do evento |
x-webhook-delivery |
ID da entrega, igual em todas as tentativas |
Todo corpo traz o nome do evento em event_name.
Validar a assinatura#
Calcule o HMAC sobre os bytes exatos recebidos, antes de converter o JSON. Se você reformatar o corpo, a assinatura deixa de bater.
import crypto from 'node:crypto';
import http from 'node:http';
const SECRET = process.env.PROMISSE_WEBHOOK_SECRET;
if (!SECRET) throw new Error('Defina PROMISSE_WEBHOOK_SECRET');
function isValid(rawBody, signature) {
const expected = Buffer.from(crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex'));
const received = Buffer.from(String(signature ?? ''));
return expected.length === received.length && crypto.timingSafeEqual(expected, received);
}
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
// Troque por um banco: a entrega é "pelo menos uma vez" e o processo pode reiniciar.
const processed = new Set();
http.createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/webhooks/promisse') return res.writeHead(404).end();
const rawBody = await readBody(req);
if (!isValid(rawBody, req.headers['x-webhook-signature'])) return res.writeHead(401).end();
const delivery = req.headers['x-webhook-delivery'];
if (processed.has(delivery)) return res.writeHead(200).end();
processed.add(delivery);
const event = JSON.parse(rawBody);
if (event.event_name === 'order_paid') {
console.log(`Pedido #${event.order.number} pago: R$ ${event.order.total}`);
}
res.writeHead(200).end();
}).listen(3000);Para testar, rode com PROMISSE_WEBHOOK_SECRET=seu_secret node server.js em um endereço público.
Eventos#
| Evento | Quando dispara | Quem pode assinar |
|---|---|---|
order_paid |
Pagamento confirmado ou pedido liberado da revisão | Quem pode ver vendas |
chat_created |
Chat aberto | Quem tem acesso total |
chat_message |
Mensagem do comprador ou da equipe | Quem tem acesso total |
feedback_create |
Avaliação recebida | Quem pode ver avaliações |
order_paid#
{
"event_name": "order_paid",
"order": {
"id": "uuid", "number": 1001, "status": "paid", "total": 15.5, "currency": "BRL", "method": "pix", "gateway": "promissepay",
"paid_at": "2026-10-31T15:04:05.000Z", "coupon": null,
"customer": { "name": "Ana Lima", "email": "ana@exemplo.com", "phone": "11999998888" },
"utm": {}
},
"items": [
{ "id": "uuid", "product_id": "uuid", "variant_id": null, "name": "Curso", "variant": null, "quantity": 1, "unit_price": 15.5 }
]
}chat_created#
chat.type é pedido ou geral. Em chat geral, pedido vem null.
{
"event_name": "chat_created",
"desc": "Evento para chats criados",
"chat": {
"id": "uuid", "pedido": "uuid", "loja": "uuid", "name": "Ana Lima", "type": "pedido", "status": "pending",
"buyer_mail": "ana@exemplo.com", "buyer_id": "uuid", "createdAt": "...", "updatedAt": "..."
}
}chat_message#
Dispara em mensagem visível do comprador ou da equipe. Mensagem de bot, boas-vindas automática e nota interna não disparam, para uma automação não reagir ao próprio bot.
message.role é buyer ou staff.
{
"event_name": "chat_message",
"store": { "id": "uuid", "name": "Minha Loja", "icon": null },
"chat": { "id": "uuid", "name": "Ana Lima", "buyer_email": "ana@exemplo.com" },
"message": {
"id": "uuid", "content": "Oi, preciso de ajuda", "role": "buyer", "role_system": null, "author": "Ana Lima",
"embeds": null, "attachments": [], "created_at": "..."
}
}feedback_create#
Dispara quando o comprador envia uma avaliação. Traz a avaliação, o item, o pedido, o produto e a variante, se houver. Valores em reais.
{
"event_name": "feedback_create",
"feedback": {
"id": "uuid", "comment": "Ótimo", "tags": [], "is_anonymous": false, "is_approved": true, "star": 5, "like": 0,
"product": "uuid", "variant": null, "cart": "uuid", "user": "uuid", "images": [], "createdAt": "..."
},
"carrinho": {
"id": "uuid", "nome": "Curso", "imagem": null, "valor": 15.5, "quantidade": 1, "produto": "uuid", "variante": null,
"categoria": "uuid", "pedido": "uuid"
},
"pedido": {
"id": "uuid", "nome": "Curso", "quantidade": 1, "total": 15.5, "metodo": "pix", "status": "paid",
"email": "ana@exemplo.com", "loja": "uuid"
},
"produto": { "id": "uuid", "nome": "Curso", "valor": 15.5, "slug": "curso", "status": true },
"variante": null
}O objeto produto traz também os demais dados do cadastro. variante vem preenchido quando a compra foi de uma variante.
webhook_test#
É o evento do botão de teste no painel. Tem uma tentativa só.
{
"event_name": "webhook_test",
"test": true,
"message": "Evento de teste enviado pelo painel da Promisse Store",
"store": { "id": "uuid", "name": "Minha Loja" },
"sent_at": "..."
}Regras de entrega#
| Regra | Como funciona |
|---|---|
| Sucesso | Resposta 2xx. |
| Tempo limite | 8 segundos. Responda rápido e processe depois. |
| Redirecionamento | Não é seguido e conta como falha. |
| Novas tentativas | Até 8, com espera crescente. |
| Fim das tentativas | Resposta 410 encerra na hora. |
| Desativação | Depois de 20 entregas seguidas com falha, o webhook é desativado. |
| Histórico | Fica por 30 dias no painel. |
Requisitos da URL#
- Em produção, precisa ser
https. - Só as portas 80 e 443.
- Não pode ter usuário e senha na URL.
- Não pode apontar para um endereço interno. A regra é conferida no cadastro e de novo a cada envio.
Trocar o Secret#
Use a opção de trocar o segredo no webhook. O Secret novo aparece uma única vez e o antigo para de valer, então atualize o seu sistema logo em seguida.

