Webhooks da Mosaqo
Enviamos um POST com corpo JSON assinado ao seu endpoint para cada evento assinado, repetindo com espera crescente até você aceitá-lo.
Assinar
Adicione um endpoint em Bulk & API ou registre um a partir do seu próprio código com uma chave webhooks:write — é assim que uma plataforma de automação liga e desliga um gatilho:
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"]
}'A resposta contém o secret de assinatura. Ele aparece uma única vez — guarde-o ou substitua a assinatura depois.
Eventos
| Evento | Dispara quando |
|---|---|
qr.created | Um código é criado, no aplicativo ou pela API |
qr.published | Um código entra no ar e seu redirecionamento passa a responder |
qr.updated | Conteúdo, design, nome ou status mudam |
qr.archived | Um código é arquivado e sai do host de redirecionamento |
qr.deleted | Um código é excluído em definitivo |
redirect.rule_changed | Um destino ou uma regra de segmentação muda |
scan.aggregate_ready | Um agregado de escaneamentos ficou pronto |
export.ready | Uma exportação assíncrona terminou |
approval.changed | Uma revisão é aprovada ou recusada |
bulk.completed | Uma tarefa em lote termina |
Payload
Quando um evento diz respeito a um QR code, data.qr traz o mesmo resumo que GET /qr/{id} devolve — assim um gatilho não precisa buscar de volta o registro sobre o qual acabou de ser avisado.
{
"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"
}
}
}Verificar uma entrega
Cada requisição traz um carimbo de tempo e uma assinatura sobre o carimbo e o corpo ao mesmo tempo:
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.publishedRecusar tudo cujo carimbo de tempo esteja longe do seu próprio relógio é exatamente o que torna irrepetível uma entrega interceptada — não pule essa checagem. Compare assinaturas em tempo constante e use o corpo bruto da requisição: reserializar o JSON já analisado muda os bytes, e a assinatura deixa de bater.
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"])Novas tentativas e falhas
Responda com qualquer 2xx assim que tiver guardado o evento; faça o trabalho depois. Tentamos até seis vezes com espera exponencial, então um handler lento vira entregas duplicadas — trate X-Mosaqo-Delivery como chave de idempotência do seu lado.
Endpoints precisam ser HTTPS em um host público. Endereços locais e privados são recusados já na criação da assinatura.
Assinatura descontinuada
X-Mosaqo-Signature-V0 traz uma assinatura mais antiga calculada só sobre o corpo. Ela não consegue expressar proteção contra repetição, segue por uma versão para os receptores existentes migrarem e não deve ser usada em nada novo.
Dados de escaneamento não são enviados
Não existe, de propósito, webhook por escaneamento. A Mosaqo não envia análises no nível do escaneamento para endpoints de terceiros, e nenhum evento da tabela acima os carrega.
O histórico de escaneamentos só fica disponível se você mesmo o buscar com sua própria chave — GET /scans — e mesmo assim ele é governado pelas configurações de privacidade analítica do seu espaço: dimensões que você desligou não aparecem em nenhuma linha, o filtro de bots se aplica, e o hash pseudônimo do visitante por trás dos “escaneamentos únicos” nunca é exposto. A resposta lista allowedDimensions para você saber o que está recebendo.
scan.aggregate_ready avisa que um agregado está pronto; ele não contém os escaneamentos em si. Se a sua integração realmente precisa de escaneamentos individuais em tempo real, isso está disponível mediante solicitação — fale com a gente em vez de supor que já existe.
Se você não puder receber webhooks
Faça consultas periódicas. GET /qr?updatedSince=… traz os códigos alterados e GET /scans?since=… percorre o histórico de escaneamentos para a frente com um cursor estável.