Saltearse al contenido

Webhooks

Webhooks

Con los webhooks recibes notificaciones en tiempo real cuando ocurren eventos en tu workspace; por ejemplo, cuando se escanea un código QR.

Crear un webhook

Ventana de terminal
POST /v1/webhooks
{
"url": "https://example.com/webhooks/qr3",
"events": ["qr.scanned", "qr.created"],
"secret": "my-secret-key-min-16-chars"
}

Respuesta

{
"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

EventDescripción
*Todos los eventos
qr.createdCódigo QR creado
qr.updatedCódigo QR actualizado (URL, estado, etiquetas)
qr.deletedCódigo QR eliminado
qr.scannedCódigo QR escaneado
qr.flaggedCódigo QR marcado como no seguro
webhook.pingPing de prueba (a través del endpoint /ping)

Formato del payload

Todos los payloads de los webhooks tienen 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 y qr.updated

En estos dos eventos, data es el código exactamente tal como lo devuelve GET /v1/codes/:id. Esto incluye appearance, los colores guardados en el código (null representa el negro o blanco predeterminado):

{
"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"
}
}

El ejemplo está abreviado. Todos los campos se encuentran en la API de códigos.

Verificación de firmas

qr3.app firma cada solicitud de webhook con HMAC-SHA256:

X-QR3-Signature: sha256=a1b2c3d4e5f6...
X-QR3-Event: qr.scanned
X-QR3-Delivery: wdl_abc123
X-QR3-Attempt: 1
User-Agent: qr3-webhooks/1.0

Implementar la verificación

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 });
},
);

Lógica de reintentos

En caso de errores (HTTP >= 300 o tiempo de espera agotado), qr3.app intentará entregar el mensaje de nuevo:

IntentoRetraso
1De inmediato
21 minuto
35 minutos

Después de 10 errores consecutivos, el webhook se desactivará automáticamente.

Garantía de entrega

qr3.app entrega al menos una vez, no exactamente una. Por eso el mismo evento puede llegar más de una vez a tu endpoint: como reintento tras un error o como repetición de un intento que ya se envió.

  • Deduplica con el id del payload (evt_…). Pertenece al evento y se mantiene igual en cada intento y en cada repetición. X-QR3-Delivery identifica el intento concreto: una repetición de ese intento lleva el mismo valor; un intento nuevo, uno nuevo. Confirma con un 2xx un id que ya conoces, no con un 4xx: un 4xx cuenta como intento fallido y, tras 10 errores consecutivos, el webhook se desactiva automáticamente.
  • Un intento que sigue en pending una hora después de crearse (por ejemplo, porque su mensaje de la cola se perdió) pasa a failed mediante una limpieza diaria; status_code y response_time_ms siguen siendo null. No es un fallo de tu endpoint y no cuenta para los 10 errores consecutivos. El registro guarda para ello el texto Delivery was not completed in time — closed by cleanup; el endpoint de registros de entrega no lo devuelve. El cierre no es definitivo: si el mensaje llega después, se entrega y su resultado real sustituye ese texto.

Registros de entrega

Ventana de terminal
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"
}
]
}

Los intentos de entrega se conservan 90 días (el día UTC en curso y los 89 anteriores) y después se eliminan automáticamente. Además de los campos mostrados, el registro almacena el payload del evento y el primer kilobyte (1 KB) de la respuesta del endpoint; este endpoint no devuelve ninguno de los dos.

Ping de prueba

Prueba un webhook de inmediato:

Ventana de terminal
POST /v1/webhooks/:id/ping

Esto envía un evento webhook.ping a la URL de tu endpoint.