Guias
Webhooks
Em vez de perguntar à Quobo o que mudou, deixe a Quobo avisar você. Quando uma movimentação acontece ou um item muda, enviamos um POST assinado para a URL que você registrar.
Como funciona
Um webhook é um endpoint HTTPS no seu servidor. Você o registra na Quobo e escolhe quais eventos quer receber. A partir daí, cada vez que um desses eventos ocorre, a Quobo faz um POST com um corpo JSON assinado para a sua URL. O seu servidor confere a assinatura, processa o evento e responde 2xx. Se responder outra coisa, ou demorar demais, a Quobo tenta de novo.
Registrar um endpoint
Disponível a partir do Starter
Webhooks fazem parte de todos os planos pagos; o plano Gratuito não os inclui. A retenção configurável dos logs de entrega é do plano Pro em diante.
Hoje você cria e gerencia seus endpoints no painel, em Configurações → Webhooks: informe um nome, a URL de destino e os eventos que quer receber. O segredo de assinatura (prefixo whsec_) aparece em texto puro uma única vez, na criação; guarde-o com segurança, pois as leituras seguintes só mostram os quatro últimos caracteres. A criação pela API, abaixo, chega junto com a API por token, em breve.
curl -X POST https://api.quobo.com.br/api/v1/webhooks/endpoints \
-H "Authorization: Bearer qbo_seu_token_aqui" \
-H "Idempotency-Key: 4f2b1c9a-3d5e-4a71-9b0c-1e2f3a4b5c6d" \
-H "Content-Type: application/json" \
-d '{
"name": "Servidor de integração",
"url": "https://api.suaempresa.com/webhooks/quobo",
"events": ["movement.recorded", "item.low_stock"]
}'{
"id": "9c8b7a6d-…",
"name": "Servidor de integração",
"url": "https://api.suaempresa.com/webhooks/quobo",
"events": ["movement.recorded", "item.low_stock"],
"active": true,
"secret": "whsec_Xa9…",
"secret_last_four": "aZ90",
"created_at": "2026-07-10T13:40:00Z"
}Eventos disponíveis
Cada endpoint recebe apenas os eventos que constam na sua lista events. Os tipos válidos são:
| Evento | Disparado quando |
|---|---|
movement.recorded | Uma entrada, saída ou venda é registrada. |
item.low_stock | Uma movimentação deixa a quantidade no ou abaixo do limite de estoque baixo, pela cascata (item, unidade, organização). |
stocktake.completed | Uma contagem física (auditoria) é concluída. |
item.created | Um item é criado no catálogo. |
item.updated | Um item do catálogo é alterado. |
subscription.updated | A assinatura da organização muda de estado. |
subscription.canceled | A assinatura da organização é cancelada. |
item.low_stock segue a cascata de limites
Este evento usa o mesmo critério da plataforma: a cascata de limites (mínimo do item, depois unidade de medida, depois padrão da organização). O campo threshold traz o limite que efetivamente valeu. Enquanto o item seguir baixo, o evento é reemitido a cada nova movimentação, então use o X-Quobo-Delivery para não processar duas vezes.
Formato da entrega
Toda entrega chega com estes cabeçalhos:
| Cabeçalho | Conteúdo |
|---|---|
Content-Type | application/json |
User-Agent | Quobo-Webhooks/1.0 |
X-Quobo-Event | O tipo do evento, ex. movement.recorded. |
X-Quobo-Timestamp | Unix (segundos) da entrega. Entra na assinatura. |
X-Quobo-Signature | HMAC-SHA256, em hexadecimal, de "<timestamp>.<corpo>". |
X-Quobo-Delivery | Identificador único da entrega, estável entre retentativas. |
O corpo tem sempre o mesmo envelope, com o conteúdo específico em data:
{
"id": "b1a2c3d4-…",
"type": "movement.recorded",
"organization_id": "9c8b7a6d-…",
"created_at": "2026-07-10T13:42:05Z",
"data": {
"movement_id": "…",
"item_id": "…",
"move_type": "sale",
"quantity": 3,
"resulting_quantity": 41
}
}Assinatura e segurança
Qualquer um pode fazer um POST para a sua URL. A assinatura é o que prova que a entrega veio da Quobo e chegou intacta. Nós e você compartilhamos o segredo whsec_ do endpoint; ninguém mais o conhece. Antes de enviar, a Quobo calcula:
HMAC_SHA256(secret, "<X-Quobo-Timestamp>.<corpo bruto>")O resultado, em hexadecimal, viaja em X-Quobo-Signature. Como o timestamp faz parte da mensagem assinada, uma captura antiga não pode ser reenviada sem que a assinatura deixe de bater com o horário atual. Para verificar:
- Leia os cabeçalhos
X-Quobo-TimestampeX-Quobo-Signature. - Recalcule o HMAC-SHA256 com o corpo bruto exatamente como recebido, antes de parsear o JSON.
- Compare com a assinatura recebida em tempo constante.
- Rejeite entregas com timestamp mais velho que 5 minutos.
import crypto from 'node:crypto'
const SECRET = process.env.QUOBO_WEBHOOK_SECRET // whsec_…
const TOLERANCE_SECONDS = 5 * 60
// Use o corpo BRUTO da requisição (Buffer/string), não o JSON já parseado.
export function isValidWebhook(rawBody, headers) {
const timestamp = headers['x-quobo-timestamp']
const signature = headers['x-quobo-signature']
if (!timestamp || !signature) return false
// Proteção contra replay: rejeite o que for mais velho que 5 minutos.
const age = Math.floor(Date.now() / 1000) - Number(timestamp)
if (!Number.isFinite(age) || Math.abs(age) > TOLERANCE_SECONDS) return false
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex')
const a = Buffer.from(expected)
const b = Buffer.from(signature)
return a.length === b.length && crypto.timingSafeEqual(a, b)
}Assine o corpo bruto, não o re-serializado
Verifique contra os bytes exatos que chegaram. Se você parsear o JSON e serializar de novo, a ordem das chaves e os espaços podem mudar, e a assinatura deixa de bater. Guarde o corpo cru antes de qualquer parse.
Idempotência e reentregas
Uma mesma entrega pode chegar mais de uma vez (uma retentativa após um timeout, por exemplo). O cabeçalho X-Quobo-Delivery é estável entre as tentativas da mesma entrega: registre os ids já processados e ignore repetições. Responda 2xx assim que aceitar o evento e faça o trabalho pesado de forma assíncrona; o seu endpoint tem 5 segundos por tentativa.
Retentativas
Uma entrega é bem-sucedida quando o seu servidor responde um status 2xx. Qualquer outra resposta, ou um tempo acima de 5 segundos, conta como falha e entra na fila de retentativa:
- Tentativasaté 5
- A entrega original mais quatro retentativas.
- Intervaloexponencial
- Backoff exponencial (fator 2) de 1s a 60s, com jitter aleatório para não concentrar as tentativas no mesmo instante.
- Timeout5s
- Tempo máximo de cada tentativa até a sua URL.
- SucessoHTTP 2xx
- Qualquer status de 200 a 299 encerra a entrega.
Você pode acompanhar o histórico de entregas e reprocessar um evento pela API de webhooks. Cada tentativa registra o último status e o último erro retornados pela sua URL.
Segredo e rotação
O segredo é por endpoint e pode ser rotacionado a qualquer momento. A rotação não tem janela de convivência: assim que você gera um novo segredo, o anterior para de validar na mesma hora. Atualize o segredo no seu servidor antes de rotacionar, para não perder entregas no intervalo.
Boas práticas
Guarde o whsec_ em variável de ambiente, nunca no código versionado. Aponte a URL para HTTPS. Rejeite tudo que não passe na verificação de assinatura, e monitore o histórico de entregas para detectar falhas cedo.