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.

bash — 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"]
  }'
Resposta 201
{
  "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:

EventoDisparado quando
movement.recordedUma entrada, saída ou venda é registrada.
item.low_stockUma movimentação deixa a quantidade no ou abaixo do limite de estoque baixo, pela cascata (item, unidade, organização).
stocktake.completedUma contagem física (auditoria) é concluída.
item.createdUm item é criado no catálogo.
item.updatedUm item do catálogo é alterado.
subscription.updatedA assinatura da organização muda de estado.
subscription.canceledA 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çalhoConteúdo
Content-Typeapplication/json
User-AgentQuobo-Webhooks/1.0
X-Quobo-EventO tipo do evento, ex. movement.recorded.
X-Quobo-TimestampUnix (segundos) da entrega. Entra na assinatura.
X-Quobo-SignatureHMAC-SHA256, em hexadecimal, de "<timestamp>.<corpo>".
X-Quobo-DeliveryIdentificador único da entrega, estável entre retentativas.

O corpo tem sempre o mesmo envelope, com o conteúdo específico em data:

Corpo (movement.recorded)
{
  "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:

A assinatura
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:

  1. Leia os cabeçalhos X-Quobo-Timestamp e X-Quobo-Signature.
  2. Recalcule o HMAC-SHA256 com o corpo bruto exatamente como recebido, antes de parsear o JSON.
  3. Compare com a assinatura recebida em tempo constante.
  4. 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.