Webhooks
A loja avisa o seu sistema quando algo muda. Os webhooks seguem a especificação Standard Webhooks: assinatura HMAC-SHA256, entrega ao menos uma vez e payload enxuto.
Como assinar
Apps recebem, ao serem instalados, os eventos aprovados no cadastro. Com um token da loja, crie endpoints em Aplicativos e API → Webhooks ou pela API, com webhooks:manage (POST /webhook_endpoints). O segredo whsec_… aparece na criação.
A requisição
Enviamos um POST HTTPS com corpo JSON e estes cabeçalhos:
| Cabeçalho | Descrição |
|---|---|
webhook-id | Id único do evento (evt_…). É o mesmo em todas as tentativas: use-o para descartar duplicados. |
webhook-timestamp | Momento do envio, em segundos Unix. Recuse se estiver a mais de 5 minutos do seu relógio. (O timestamp do corpo é o momento da mudança.) |
webhook-signature | v1,<base64>. Durante a troca de segredo pode haver várias assinaturas, separadas por espaço. |
POST /webhooks/tfshop HTTP/1.1
Host: erp.exemplo.com.br
Content-Type: application/json
webhook-id: evt_2Wm9QeR4tY7uI1
webhook-timestamp: 1793279112
webhook-signature: v1,oko9mPul9WX3z0xI398oWm44c9uaHY5FAAL/CR1PWno=
{
"id": "evt_2Wm9QeR4tY7uI1",
"type": "order.paid",
"timestamp": "2026-10-29T13:05:12.000Z",
"api_version": "v1",
"store_id": "st_volt",
"actor": { "type": "system", "app_id": null },
"data": {
"object": "order",
"id": "o_1011",
"reference": "1011",
"status": "paid",
"test": false
}
}Payload enxuto
O evento traz só ids, uma referência (número do pedido, SKU, chave da NF-e) e o estado, nunca dados pessoais. Busque o objeto atualizado com GET: assim o acesso é sempre conferido contra os escopos do token.
Responda rápido
Responda 2xx em até 10 segundos e processe depois, numa fila. Outro status, timeout ou erro de conexão gera nova tentativa. Redirecionamentos não são seguidos.
Ao menos uma vez, sem ordem garantida
- Guarde os
webhook-idjá processados e ignore os repetidos. - Eventos podem chegar fora de ordem: compare o
updated_atdo objeto obtido no GET com o que você já tem. - Ignore os eventos causados por você mesmo (
actor.app_id).
Novas tentativas
Sem 2xx, tentamos de novo neste ritmo: 8 tentativas ao longo de cerca de 27 h 35 min.
| Tentativa | Espera desde a anterior | Tempo desde o evento |
|---|---|---|
| 1 | imediata | 0 |
| 2 | 5 s | 5 s |
| 3 | 5 min | 5 min |
| 4 | 30 min | 35 min |
| 5 | 2 h | 2 h 35 min |
| 6 | 5 h | 7 h 35 min |
| 7 | 10 h | 17 h 35 min |
| 8 | 10 h | 27 h 35 min |
Reenvio e recuperação
- O lojista pode reenviar uma entrega em Aplicativos e API → Webhooks, com o mesmo
webhook-id, e mandar um evento de teste (webhook.test) para conferir o endpoint. - Depois de uma queda, recupere o que perdeu com
GET /v1/events?created_since=…: os eventos ficam disponíveis por 30 dias.
curl "https://api.tfshop.cloud/v1/events?created_since=2026-10-28T00:00:00Z&type=order.paid&limit=100" \
-H "Authorization: Bearer $TFSHOP_TOKEN"Catálogo de eventos
| Evento | Quando | Escopo | Objeto |
|---|---|---|---|
order.created | Pedido criado, inclusive Pix ou boleto aguardando pagamento. | orders:read | order |
order.paid | Pagamento confirmado pelo C6. É o gatilho padrão de exportação para o ERP. | orders:read | order |
order.updated | Pedido alterado (endereço corrigido, observações, situação do envio…). | orders:read | order |
order.cancelled | Pedido cancelado ou expirado. | orders:read | order |
refund.completed | Reembolso total ou parcial concluído pela loja. | orders:read | order |
invoice.authorized | NF-e autorizada registrada no pedido (TF Fiscal ou ERP). | orders:read | invoice |
invoice.cancelled | NF-e cancelada. | orders:read | invoice |
fulfillment.shipped | Pedido enviado, com transportadora e rastreio. | orders:read | fulfillment |
fulfillment.delivered | Pedido entregue. | orders:read | fulfillment |
product.created | Produto criado. | products:read | product |
product.updated | Produto ou preço alterado. | products:read | product |
product.deleted | Produto arquivado. | products:read | product |
inventory.updated | O estoque de um SKU mudou. | inventory:read | inventory_level |
app.uninstalled | O lojista desinstalou o seu app; os tokens já foram revogados. | orders:read | app |
Verifique a assinatura
webhook-id + . + webhook-timestamp + . + corpo bruto. Calcule o HMAC-SHA256 com a chave (a parte depois de whsec_, decodificada de base64), codifique em base64 e compare em tempo constante com cada assinatura v1, do cabeçalho. Use o corpo bruto, antes de qualquer parse de JSON.import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.TFSHOP_WEBHOOK_SECRET; // "whsec_..."
const KEY = Buffer.from(SECRET.slice("whsec_".length), "base64");
const TOLERANCE_S = 5 * 60;
function verify(headers, rawBody /* Buffer */) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures) return false;
const ts = Number(timestamp);
const now = Math.floor(Date.now() / 1000);
if (!Number.isInteger(ts) || Math.abs(now - ts) > TOLERANCE_S) return false;
const expected = crypto
.createHmac("sha256", KEY)
.update(`${id}.${timestamp}.`)
.update(rawBody)
.digest();
return signatures.split(" ").some((entry) => {
const [version, signature] = entry.split(",");
if (version !== "v1" || !signature) return false;
const received = Buffer.from(signature, "base64");
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
// express.raw keeps the exact bytes that were signed
app.post("/webhooks/tfshop", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.headers, req.body)) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
// dedupe on event.id (= webhook-id), enqueue, answer fast
res.status(204).end();
});Teste a sua implementação
Calcule a assinatura esperada e compare com a do seu código. Os valores iniciais são os do exemplo acima. O cálculo é feito no seu navegador; nada é enviado.