Webhooks
Receba notificações push em tempo real quando coisas acontecem na sua conta Quotery. Sem polling, sem cron jobs. Apenas callbacks HTTP assinados entregues direto ao seu servidor.
O que são webhooks?
Um webhook é uma notificação push que o Quotery envia ao seu servidor sempre que algo importante acontece. Um orçamento é aceito, uma nota de entrega é marcada como entregue ou uma nota de devolução é criada. Em vez de você perguntar "alguma novidade?" repetidamente (polling, um desperdício), nós te avisamos no momento em que o evento dispara.
Você nos informa uma URL e quais tipos de evento te interessam. Quando um desses eventos acontece na sua conta, enviamos um payload JSON assinado via POST para sua URL. Você verifica a assinatura para confirmar que veio de nós, processa os dados e retorna um 200. Só isso.
Webhooks são a espinha dorsal das integrações em tempo real. Use para enviar pedidos para seu ERP, sincronizar inventário com um sistema externo, disparar notificações no Slack quando um orçamento fecha ou criar um registro de auditoria de tudo que acontece no seu tenant. Você escolhe os eventos, você gerencia o receptor.
Tipos de evento disponíveis
Cada tipo de evento representa uma ação de negócio específica dentro do Quotery. Você assina os que são relevantes para sua integração. Escolha alguns ou todos os sete.
quote.closed— Dispara quando um orçamento é fechado (finalizado e enviado ao cliente). O payload inclui os detalhes do orçamento, itens e totais no momento do fechamento.quote.accepted— Dispara quando um cliente aceita um orçamento pelo portal. É o evento principal para iniciar a expedição: crie um pedido no ERP, notifique o almoxarifado ou gere uma nota fiscal.quote.cancelled— Dispara quando um orçamento é cancelado. Use para desfazer processos: liberar inventário reservado, cancelar um pedido ou atualizar o estágio do negócio no CRM.delivery_note.created— Dispara quando uma nota de entrega é criada. Se você acompanha envios num sistema externo, este evento avisa que uma nova entrega está sendo preparada.delivery_note.marked_delivered— Dispara quando uma nota de entrega é marcada como entregue. É a confirmação de que as mercadorias chegaram. Use para reduzir inventário, fechar uma tarefa de expedição ou disparar uma fatura.stock_receipt.completed— Dispara quando um recebimento de estoque é concluído. As mercadorias recebidas agora estão no seu inventário. Sincronize com seu WMS ou atualize níveis de estoque num catálogo externo.return_note.created— Dispara quando uma nota de devolução é criada. Um cliente está devolvendo algo. Você vai querer ajustar inventário, iniciar um fluxo de reembolso ou registrar a devolução no seu sistema de pedidos.
Criando um webhook
Configurar um webhook leva cerca de um minuto. Você precisa de acesso admin à sua conta Quotery. O gerenciamento de webhooks é uma função administrativa.
- Escolha um nome e uma URL de destino
Dê um nome legível ao seu webhook, algo como "Integração ERP Produção" para sua equipe saber o que ele faz. A URL de destino deve ser um endpoint HTTPS que aceita requisições POST com corpo JSON. É o seu servidor, ou um serviço como Zapier, Make ou um webhook de entrada do Slack. - Escolha seus tipos de evento
Escolha os eventos que quer assinar. Pode selecionar alguns (só quote.accepted e quote.closed) ou todos os sete. Só os eventos marcados disparam entregas para sua URL. Você pode atualizar esta lista depois sem criar um novo webhook. - Crie via API
Webhooks são gerenciados pela API REST em /api/v1/webhooks/. Você faz um POST com um corpo JSON contendo nome, URL e lista de eventos. A resposta inclui um signing_secret. Salve-o imediatamente. Ele é mostrado uma vez e nunca mais. Se perder, precisa criar um novo webhook. - Armazene o segredo de assinatura com segurança
Trate o segredo de assinatura como senha. Armazene como variável de ambiente no seu servidor (QUOTERY_WEBHOOK_SECRET é um bom nome). Você vai usá-lo para verificar cada entrega. Qualquer pessoa com acesso a este segredo pode forjar payloads de webhook. - Verifique se as entregas estão chegando
Acione um evento real na sua conta Quotery (feche um orçamento, marque uma nota como entregue) e verifique se seu endpoint recebe o POST. Você pode inspecionar a saúde de entrega do webhook (timestamp do último sucesso e contagem de falhas) a qualquer momento pelo endpoint GET. Verde significa funcionando.
O segredo de assinatura é gerado automaticamente quando você cria o webhook. Você não o escolhe, você o recebe. E você só o vê uma vez, logo após a criação. Copie para um lugar seguro antes de fechar aquela aba de resposta.
Você precisa estar logado como admin para criar ou gerenciar webhooks. Se não for admin, procure alguém da sua equipe que seja.
Verificando assinaturas
Cada entrega de webhook inclui uma assinatura criptográfica para você confirmar que o payload veio do Quotery e não foi adulterado. Verificar é obrigatório. Nunca processe um payload de webhook sem verificar a assinatura primeiro.
Nós assinamos o corpo bruto da requisição com HMAC-SHA256 usando o segredo de assinatura único do seu webhook. A assinatura vai no cabeçalho X-Quotery-Signature num formato compatível com o esquema do Stripe. Se você já trabalhou com webhooks do Stripe, vai achar familiar.
Formato da assinatura
O cabeçalho X-Quotery-Signature se parece com isto:
t=1714789200,v1=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2- t
- O timestamp Unix (em segundos) de quando a assinatura foi gerada. Seu código de verificação deve rejeitar entregas com timestamp mais antigo que 5 minutos. Isso previne ataques de replay.
- v1
- O digest hexadecimal HMAC-SHA256 do corpo da requisição, calculado com seu segredo de assinatura.
Python
import hmac, hashlib, time
def verify_signature(body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > tolerance:
return False # outside tolerance window
expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"], expected)O parâmetro de tolerância (padrão 300 segundos = 5 minutos) rejeita entregas antigas. É sua proteção contra ataques de replay. Ajuste se seus relógios estiverem dessincronizados, mas não defina acima de 5 minutos.
Node.js
const crypto = require("crypto");
function verifySignature(rawBody, signatureHeader, secret, tolerance = 300) {
// Parse "t=1714789200,v1=abc123..." into { t, v1 }
const parts = {};
signatureHeader.split(",").forEach(p => {
const idx = p.indexOf("=");
parts[p.slice(0, idx)] = p.slice(idx + 1);
});
const timestamp = parseInt(parts.t, 10);
const now = Math.floor(Date.now() / 1000);
// Reject if outside tolerance window (5 minutes)
if (Math.abs(now - timestamp) > tolerance) {
return false;
}
// Compute the expected signature
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
// Constant-time comparison prevents timing attacks
return crypto.timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected)
);
}A verificação de assinatura usa timingSafeEqual em vez de comparação de strings comum. Isso é importante. Operadores de igualdade comuns causam curto-circuito, vazando informação de tempo sobre quanto da assinatura coincidiu. Sempre use comparação de tempo constante para verificação HMAC.
Cabeçalhos de entrega
Cada entrega POST inclui estes quatro cabeçalhos. Você os usa para verificação de assinatura, idempotência e decisões de roteamento.
| Header | Value |
|---|---|
| Content-Type | application/json |
| X-Quotery-Webhook-Id | UUID (ex. 550e8400-e29b-41d4-a716-446655440000) |
| X-Quotery-Event | Tipo de evento separado por ponto (ex. quote.closed) |
| X-Quotery-Signature | t=1714789200,v1=abc123... |
Content-Type — O corpo da requisição é sempre JSON. Seu endpoint pode assumir isso e fazer o parsing.
X-Quotery-Webhook-Id — Um identificador único para esta tentativa de entrega. Use como chave de idempotência. Se uma entrega for reenviada, carrega o mesmo ID. Você deduplica verificando se já processou este ID.
X-Quotery-Event — Informa exatamente qual evento disparou. Use num switch para rotear eventos diferentes para manipuladores diferentes.
X-Quotery-Signature — A assinatura HMAC-SHA256. Faça o parsing, verifique e só processe o payload se for válida.
Comportamento de retentativa
Se seu endpoint estiver fora do ar ou retornar um status não-2xx, o Quotery reenvia a entrega automaticamente. Você não precisa configurar nada. A agenda de retentativas já vem embutida.
Agenda de retentativas
Entregas com falha são reenviadas até 6 vezes usando backoff exponencial. Esta é a linha do tempo a partir da tentativa inicial:
- Attempt 1: 1 minuto após a primeira falha
- Attempt 2: 5 minutos após a primeira falha
- Attempt 3: 15 minutos após a primeira falha
- Attempt 4: 1 hora após a primeira falha
- Attempt 5: 4 horas após a primeira falha
- Attempt 6: 12 horas após a primeira falha
A janela total de retentativas cobre cerca de 12 horas. Após 6 tentativas com falha, a entrega é descartada permanentemente.
Acompanhando a saúde das entregas
Cada webhook rastreia dois indicadores: o timestamp da última entrega bem-sucedida e uma contagem de falhas consecutivas. Uma entrega bem-sucedida zera a contagem. Uma sequência de falhas a incrementa. Você verifica ambos a qualquer momento pela API. Sem logs para vasculhar, sem alertas para configurar. Se a contagem de falhas estiver subindo, seu endpoint provavelmente está fora do ar ou rejeitando entregas.
Como as retentativas reutilizam o mesmo X-Quotery-Webhook-Id, seu endpoint pode lidar com segurança com entregas duplicadas. Mantenha um pequeno cache de IDs de webhook processados recentemente (algumas horas bastam, considerando a janela de 12 horas) e pule qualquer entrega cujo ID você já tenha visto.
Estrutura do payload
Cada entrega de webhook carrega o mesmo envelope externo. Os detalhes específicos do evento ficam dentro do objeto data.
{
"event_type": "quote.closed",
"tenant_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"timestamp": "2026-05-03T10:00:00Z",
"data": {
// Payload específico do evento — varia por event_type
}
}- event_type
- O tipo de evento separado por ponto da lista acima (ex. quote.closed). Use para rotear o payload ao manipulador correto na sua integração.
- tenant_id
- O UUID do seu tenant Quotery. Se sua integração atende vários tenants, use para restringir o processamento à conta correta.
- timestamp
- Um timestamp UTC ISO 8601 de quando o evento disparou. Útil para ordenar eventos ou detectar entregas atrasadas.
- data
- O payload específico do evento. Para um evento de orçamento, inclui o ID do orçamento, detalhes do cliente, itens e totais. Para uma nota de entrega, inclui o ID da nota, itens e status. O formato exato depende do tipo de evento e da representação serializada da entidade no momento do disparo.
Testando seu webhook
Você não precisa de um ambiente de staging para testar webhooks. Pode disparar eventos reais na sua conta Quotery e vê-los chegando ao seu endpoint em tempo real.
- Use o webhook.site para testes iniciais
O webhook.site fornece uma URL temporária que registra cada requisição recebida. Crie um webhook apontado para uma URL do webhook.site primeiro. Assim você inspeciona os cabeçalhos e o formato do payload antes de escrever código. Quando estiver satisfeito, mude a URL para seu endpoint real. - Dispare um evento real
Realize a ação no Quotery que corresponde ao seu evento assinado: feche um orçamento, marque uma nota como entregue, conclua um recebimento. O webhook dispara automaticamente no momento em que o evento é salvo. Sem apertar botão extra. - Verifique a saúde da entrega
Acesse GET /api/v1/webhooks/ para ver last_success_at e failure_count do seu webhook. Se last_success_at foi atualizado e failure_count é zero, seu endpoint recebeu e confirmou. Se failure_count está subindo, verifique se seu endpoint está acessível, retornando 2xx e respondendo em até 10 segundos. - Itere sobre seu manipulador
Use os dados reais do payload para desenvolver sua lógica de integração. Faça o parsing de event_type para rotear, extraia os campos que precisa do data e mapeie para seu sistema. O padrão de cabeçalhos e verificação de assinatura é o mesmo para qualquer tipo de evento.
Exemplos de código
Pronto para conectar? Veja as duas tarefas mais comuns (criar um webhook e receber entregas) em formato para copiar e colar.
Criando um webhook com curl
Este comando curl cria um webhook que escuta os eventos quote.accepted, quote.closed e delivery_note.marked_delivered. Você precisa do cookie de sessão de uma sessão de navegador admin autenticada.
curl -X POST https://app.quotery.io/api/v1/webhooks/ \
-H "Content-Type: application/json" \
-H "Cookie: sessionid=<seu-id-de-sessao-admin>" \
-d '{
"name": "Minha Integração",
"url": "https://meuapp.exemplo.com/webhooks/quotery",
"events": ["quote.accepted", "quote.closed", "delivery_note.marked_delivered"]
}'A resposta 201 inclui o ID do webhook e, ponto crítico, o signing_secret. Copie esse segredo agora. Você não o verá novamente.
Endpoint receptor em Node.js
Aqui está um endpoint Express mínimo que verifica a assinatura, confere a tolerância de timestamp e deduplica por ID de webhook. Incorpore no seu app Node existente ou use como ponto de partida.
const express = require("express");
const crypto = require("crypto");
const app = express();
const SECRET = process.env.QUOTERY_WEBHOOK_SECRET;
const TOLERANCE = 300; // 5 minutos em segundos
// Armazenamento de idempotência em memória — use Redis ou um BD em produção
const processedIds = new Set();
app.post("/webhooks/quotery", express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}), (req, res) => {
// 1. Fazer parsing do cabeçalho de assinatura
const header = req.headers["x-quotery-signature"];
if (!header) return res.status(400).send("Assinatura ausente");
const parts = {};
header.split(",").forEach(p => {
const idx = p.indexOf("=");
parts[p.slice(0, idx)] = p.slice(idx + 1);
});
// 2. Rejeitar se o timestamp estiver fora da tolerância
const timestamp = parseInt(parts.t, 10);
if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > TOLERANCE) {
return res.status(400).send("Timestamp fora da tolerância");
}
// 3. Verificar a assinatura HMAC
const expected = crypto
.createHmac("sha256", SECRET)
.update(req.rawBody)
.digest("hex");
const valid = crypto.timingSafeEqual(
Buffer.from(parts.v1),
Buffer.from(expected)
);
if (!valid) return res.status(400).send("Assinatura inválida");
// 4. Deduplicar por ID de webhook
const deliveryId = req.headers["x-quotery-webhook-id"];
if (processedIds.has(deliveryId)) {
return res.sendStatus(200); // Já processado — confirmar silenciosamente
}
processedIds.add(deliveryId);
// 5. Processar o evento
const eventType = req.headers["x-quotery-event"];
const payload = req.body;
console.log(`Recebido ${eventType}`, payload);
switch (eventType) {
case "quote.accepted":
// Criar um pedido no seu ERP
break;
case "quote.closed":
// Atualizar o estágio do negócio no seu CRM
break;
case "delivery_note.marked_delivered":
// Reduzir inventário, disparar fatura
break;
default:
console.log(`Tipo de evento não tratado: ${eventType}`);
}
res.sendStatus(200);
});
app.listen(process.env.PORT || 3000, () => {
console.log("Receptor de webhook escutando");
});A verificação de idempotência na etapa 4 é importante. O Quotery reenvia entregas até 6 vezes e cada retentativa carrega o mesmo X-Quotery-Webhook-Id. Sem deduplicação, você processaria o mesmo evento várias vezes: pedidos duplicados, contagens de inventário em dobro e por aí vai.