Saltar al contenido

Webhooks de Mosaqo

Enviamos un POST con cuerpo JSON firmado a tu endpoint por cada evento al que estés suscrito, y reintentamos con espera creciente hasta que lo aceptes.

Suscribirse

Agrega un endpoint en Bulk & API o regístralo desde tu propio código con una llave webhooks:write: así es como una plataforma de automatización prende y apaga un disparador.

curl -X POST https://api.mosaqo.app/v1/public-api/webhooks \
  -H "Authorization: Bearer $MOSAQO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/mosaqo",
    "eventTypes": ["qr.published", "qr.updated"]
  }'

La respuesta contiene el secret de firma. Solo se muestra una vez: guárdalo o reemplaza la suscripción más adelante.

Eventos

EventoSe dispara cuando
qr.createdSe crea un código, en la aplicación o mediante la API
qr.publishedUn código entra en línea y su redirección empieza a responder
qr.updatedCambian el contenido, el diseño, el nombre o el estado
qr.archivedSe archiva un código y se retira del host de redirecciones
qr.deletedSe elimina un código de forma permanente
redirect.rule_changedCambia un destino o una regla de segmentación
scan.aggregate_readyUn agregado de escaneos quedó listo
export.readyTerminó una exportación asíncrona
approval.changedUna revisión se aprueba o se rechaza
bulk.completedTermina un trabajo por lotes
feedback.response_receivedAlguien responde un formulario de feedback
feedback.low_score_receivedUna respuesta queda por debajo del umbral de nota baja
feedback.response_resolvedUna respuesta se marca como resuelta en la bandeja
feedback.campaign_publishedUna ubicación de feedback se publica

Payload

Cuando un evento tiene que ver con un código QR, data.qr lleva el mismo resumen que devuelve GET /qr/{id}, de modo que un disparador no tiene que volver a pedir el registro del que acaban de avisarle.

{
  "id": "0b0f…",
  "type": "qr.published",
  "workspaceId": "2f60…",
  "occurredAt": "2026-08-05T10:20:30.456Z",
  "data": {
    "qrId": "e7b7…",
    "source": "public_api",
    "qr": {
      "id": "e7b7…",
      "name": "Autumn campaign",
      "slug": "autumn-campaign-1a2b3c4d",
      "mode": "dynamic",
      "contentType": "url",
      "status": "published",
      "approvalState": "not_required",
      "folderId": null,
      "updatedAt": "2026-08-05T10:20:30.401Z",
      "publicUrl": "https://mosaqo.link/aB3dEf"
    }
  }
}

Eventos de feedback

Cuatro eventos cubren el módulo de feedback y son toda su superficie de integración. Cada entrega lleva identificadores, la nota y su tipo, nunca lo que alguien escribió: el cuerpo de un webhook acaba donde lo guarde quien lo recibe, y esa no es una decisión que el espacio de trabajo tome por su cliente.

{
  "id": "7c41…",
  "type": "feedback.response_received",
  "workspaceId": "2f60…",
  "occurredAt": "2026-08-05T10:20:30.456Z",
  "data": {
    "responseId": "9d22…",
    "campaignId": "3e80…",
    "placementId": "b5ae…",
    "score": 9,
    "scoreKind": "nps"
  }
}

feedback.low_score_received solo se envía mientras esté activada la notificación de nota baja en el espacio de trabajo, y solo por debajo del umbral fijado allí. feedback.response_received se envía para cada respuesta en cualquier caso: suscríbete a ese si quieres todas.

No hay endpoint con clave de API para las respuestas. El módulo las lee por una ruta autenticada por sesión que una clave no alcanza, así que las respuestas salen de la bandeja en CSV: escríbenos en lugar de suponer que existe una ruta de descarga.

Verificar una entrega

Cada petición lleva una marca de tiempo y una firma sobre la marca y el cuerpo a la vez:

X-Mosaqo-Timestamp: 1785942718
X-Mosaqo-Signature: t=1785942718,v1=<hex HMAC-SHA256 of "{timestamp}.{raw body}">
X-Mosaqo-Delivery: 0b0f…
X-Mosaqo-Event: qr.published

Rechazar todo aquello cuya marca de tiempo esté lejos de tu propio reloj es justo lo que vuelve irrepetible una entrega interceptada, así que no te saltes esa comprobación. Compara firmas en tiempo constante y usa el cuerpo crudo de la petición: volver a serializar el JSON ya analizado cambia los bytes y la firma dejará de coincidir.

Node.js

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyMosaqoWebhook(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(',').map((piece) => piece.split('=').map((s) => s.trim())),
  );
  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest();
  const received = Buffer.from(parts.v1 ?? '', 'hex');
  return expected.length === received.length && timingSafeEqual(expected, received);
}

PHP

<?php
function verify_mosaqo_webhook(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
    $parts = [];
    foreach (explode(',', $header) as $piece) {
        [$key, $value] = array_map('trim', explode('=', $piece, 2));
        $parts[$key] = $value;
    }
    if (!isset($parts['t'], $parts['v1'])) return false;
    if (abs(time() - (int) $parts['t']) > $tolerance) return false;

    $expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1']);
}

Python

import hmac, time
from hashlib import sha256

def verify_mosaqo_webhook(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(piece.strip().split("=", 1) for piece in header.split(","))
    if "t" not in parts or "v1" not in parts:
        return False
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False

    signed = f"{parts['t']}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Reintentos y fallas

Responde con cualquier 2xx en cuanto hayas guardado el evento y haz el trabajo después. Reintentamos hasta seis veces con espera exponencial, así que un manejador lento se traduce en entregas duplicadas: trata X-Mosaqo-Delivery como llave de idempotencia de tu lado.

Los endpoints deben ser HTTPS en un host público. Las direcciones locales y privadas se rechazan al crear la suscripción.

Firma obsoleta

X-Mosaqo-Signature-V0 lleva una firma anterior calculada solo sobre el cuerpo. No puede expresar protección contra repeticiones, se manda durante una versión para que los receptores existentes migren y no debe usarse para nada nuevo.

Los datos de escaneo no se envían

Deliberadamente no hay webhook por escaneo. Mosaqo no envía analíticas a nivel de escaneo a endpoints de terceros, y ningún evento de la tabla anterior las lleva.

El historial de escaneos solo está disponible si lo obtienes tú mismo con tu propia llave —GET /scans— e incluso entonces lo rigen los ajustes de privacidad analítica de tu espacio: las dimensiones que hayas desactivado no aparecen en ninguna fila, se aplica el filtro de bots y el hash seudónimo del visitante detrás de los «escaneos únicos» nunca se expone. La respuesta lista allowedDimensions para que sepas qué estás recibiendo.

scan.aggregate_ready indica que un agregado está listo; no contiene los escaneos en sí. Si tu integración de verdad necesita escaneos individuales en tiempo real, eso está disponible bajo petición: háblanos de ello en lugar de darlo por sentado.

Si no puedes recibir webhooks

Consulta de forma periódica. GET /qr?updatedSince=… devuelve los códigos modificados y GET /scans?since=… recorre el historial de escaneos hacia adelante con un cursor estable.