Webhooks
Webhooks
Com os webhooks, você recebe notificações em tempo real quando ocorrem eventos no seu workspace — por exemplo, quando um código QR é escaneado.
Criar um Webhook
POST /v1/webhooks{ "url": "https://example.com/webhooks/qr3", "events": ["qr.scanned", "qr.created"], "secret": "my-secret-key-min-16-chars"}Resposta
{ "id": "wh_abc123", "url": "https://example.com/webhooks/qr3", "events": ["qr.scanned", "qr.created"], "is_active": true, "secret": "my-secret-key-min-16-chars", "secret_hint": "my-s…", "created_at": "2026-03-15T10:00:00.000Z"}Tipos de Eventos
| Evento | Descrição |
|---|---|
* | Todos os eventos |
qr.created | Código QR criado |
qr.updated | Código QR atualizado (URL, status, tags) |
qr.deleted | Código QR excluído |
qr.scanned | Código QR escaneado |
qr.flagged | Código QR marcado como inseguro |
webhook.ping | Ping de teste (via endpoint /ping) |
Formato do Payload
Todos os payloads de webhook têm este formato:
{ "id": "evt_4f0c2a91d7e84b3c9a1f", "type": "qr.scanned", "created": "2026-03-15T10:00:00.000Z", "data": { "code_id": "qr_abc123", "short_code": "r7f3Kx", "scanned_at": "2026-03-15T10:00:00.000Z", "country": "AT", "device_type": "mobile", "os": "iOS", "browser": "Safari", "language": "de", "ab_variant": null, "redirected_to": "https://example.com/landing" }}Eventos de código: qr.created e qr.updated
Nestes dois eventos, data é o código exatamente como GET /v1/codes/:id o fornece. Isso inclui appearance, as cores salvas no código (null representa o preto ou branco padrão):
{ "id": "evt_9b2e41c07a5d4f18b3c6", "type": "qr.updated", "created": "2026-09-28T10:00:00.000Z", "data": { "id": "qr_abc123", "short_code": "r7f3Kx", "status": "live", "appearance": { "foreground_color": "#1F4E79", "background_color": null }, "updated_at": "2026-09-28T10:00:00.000Z" }}O exemplo está abreviado. Todos os campos estão disponíveis na API de Códigos.
Verificação de Assinatura
O qr3.app assina cada requisição de webhook com HMAC-SHA256:
X-QR3-Signature: sha256=a1b2c3d4e5f6...X-QR3-Event: qr.scannedX-QR3-Delivery: wdl_abc123X-QR3-Attempt: 1User-Agent: qr3-webhooks/1.0Implementar a Verificação
import crypto from "crypto";
function verifySignature( payload: string, signature: string | undefined, secret: string): boolean { const expected = crypto .createHmac("sha256", secret) .update(payload) .digest("hex"); const expectedBytes = Buffer.from(`sha256=${expected}`); const receivedBytes = Buffer.from(signature ?? ""); return ( expectedBytes.length === receivedBytes.length && crypto.timingSafeEqual(expectedBytes, receivedBytes) );}
// In Express: verify the raw body — re-serialised JSON breaks the signature.app.post( "/webhooks/qr3", express.raw({ type: "application/json" }), (req, res) => { const raw = req.body.toString("utf8"); const signature = req.headers["x-qr3-signature"] as string | undefined; if (!verifySignature(raw, signature, process.env.QR3_WEBHOOK_SECRET!)) { return res.status(401).json({ error: "Invalid signature" }); } const event = JSON.parse(raw); console.log(event.type, event.data); res.json({ received: true }); },);import hmacimport hashlib
def verify_signature(payload: str, signature: str, secret: str) -> bool: expected = hmac.new( secret.encode(), payload.encode(), hashlib.sha256 ).hexdigest() received = (signature or "").removeprefix("sha256=") return hmac.compare_digest(expected, received)Lógica de Tentativas
Em caso de erros (HTTP >= 300 ou timeout), o qr3.app tenta reenviar a entrega:
| Tentativa | Atraso |
|---|---|
| 1 | Imediatamente |
| 2 | 1 minuto |
| 3 | 5 minutos |
Após 10 erros consecutivos, o webhook é desativado automaticamente.
Garantia de Entrega
O qr3.app entrega pelo menos uma vez, não exatamente uma. Por isso, o mesmo evento pode chegar mais de uma vez ao seu endpoint: como nova tentativa após um erro ou como repetição de uma tentativa que já foi enviada.
- Evite duplicidades usando o
iddo payload (evt_…). Ele pertence ao evento e permanece igual em cada tentativa e em cada repetição.X-QR3-Deliveryidentifica a tentativa individual: uma repetição dessa tentativa traz o mesmo valor, uma nova tentativa traz um valor novo. Confirme com 2xx umidque você já conhece, não com 4xx: um 4xx conta como tentativa com falha e, após 10 erros consecutivos, o webhook é desativado automaticamente. - Uma tentativa que ainda está
pendinguma hora após ser criada (por exemplo, porque sua mensagem na fila se perdeu) é definida comofailedpor uma limpeza diária;status_codeeresponse_time_mspermanecemnull. Isso não é uma falha do seu endpoint e não conta para os 10 erros consecutivos. O log armazena para isso o textoDelivery was not completed in time — closed by cleanup; o endpoint dos logs de envio não o retorna. O fechamento não é definitivo: se a mensagem ainda chegar, ela é entregue e seu resultado real substitui esse texto.
Logs de Envio
GET /v1/webhooks/:id/deliveries{ "data": [ { "id": "wdl_abc123", "event_type": "qr.scanned", "status": "success", "status_code": 200, "response_time_ms": 145, "attempt": 1, "created_at": "2026-03-15T10:00:00.000Z" } ]}As tentativas de entrega são mantidas por 90 dias (o dia UTC atual e os 89 anteriores) e depois eliminadas automaticamente. Além dos campos mostrados, o log armazena o payload do evento e o primeiro quilobyte (1 KB) da resposta do endpoint; este endpoint não retorna nenhum dos dois.
Ping de Teste
Teste um webhook imediatamente:
POST /v1/webhooks/:id/pingIsso envia um evento webhook.ping para a URL do seu endpoint.