Przejdź do treści

Webhooki Mosaqo

Dla każdego subskrybowanego zdarzenia wysyłamy POST z podpisanym JSON-em na Twój endpoint i ponawiamy z rosnącym odstępem, aż go przyjmiesz.

Subskrypcja

Dodaj endpoint w sekcji Bulk & API albo zarejestruj go z własnego kodu kluczem z zakresem webhooks:write — tak właśnie platforma automatyzacji włącza i wyłącza wyzwalacz:

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"]
  }'

Odpowiedź zawiera secret do podpisu. Pokazujemy go tylko raz — zapisz go albo później podmień subskrypcję.

Zdarzenia

ZdarzenieWyzwala się, gdy
qr.createdKod zostaje utworzony, w aplikacji lub przez API
qr.publishedKod wchodzi na żywo, a jego przekierowanie zaczyna działać
qr.updatedZmienia się treść, projekt, nazwa lub status
qr.archivedKod trafia do archiwum i znika z hosta przekierowań
qr.deletedKod zostaje trwale usunięty
redirect.rule_changedZmienia się cel lub reguła targetowania
scan.aggregate_readyAgregat skanowań jest gotowy
export.readyEksport asynchroniczny się zakończył
approval.changedRecenzja zostaje zatwierdzona lub odrzucona
bulk.completedZadanie zbiorcze się kończy

Payload

Gdy zdarzenie dotyczy kodu QR, data.qr niesie to samo podsumowanie, które zwraca GET /qr/{id} — wyzwalacz nie musi więc dopytywać o rekord, o którym właśnie mu powiedziano.

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

Weryfikacja dostarczenia

Każde żądanie niesie znacznik czasu i podpis obejmujący zarówno znacznik, jak i treść:

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

Odrzucanie wszystkiego, czego znacznik czasu jest daleko od Twojego zegara, to dokładnie to, co czyni przechwycone dostarczenie niemożliwym do powtórzenia — nie pomijaj tej kontroli. Porównuj podpisy w stałym czasie i używaj surowej treści żądania: ponowna serializacja sparsowanego JSON-a zmienia bajty i podpis przestanie się zgadzać.

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"])

Ponowienia i błędy

Odpowiedz dowolnym 2xx, gdy tylko zapiszesz zdarzenie; pracę wykonaj później. Ponawiamy do sześciu razy z wykładniczym odstępem, więc wolna obsługa zamienia się w zdublowane dostarczenia — traktuj X-Mosaqo-Delivery po swojej stronie jako klucz idempotentności.

Endpointy muszą być po HTTPS na publicznym hoście. Adresy lokalne i prywatne odrzucamy już przy tworzeniu subskrypcji.

Wycofywany podpis

X-Mosaqo-Signature-V0 niesie starszy podpis liczony wyłącznie z treści. Nie potrafi wyrazić ochrony przed powtórzeniem, jedzie jeszcze jedno wydanie, by istniejący odbiorcy zdążyli się przenieść, i nie należy go używać do niczego nowego.

Danych o skanowaniach nie wypychamy

Celowo nie ma webhooka na pojedyncze skanowanie. Mosaqo nie wysyła analityki na poziomie skanowania do endpointów osób trzecich i żadne zdarzenie z powyższej tabeli jej nie niesie.

Historia skanowań jest dostępna wyłącznie wtedy, gdy pobierzesz ją sam własnym kluczem — GET /scans — i nawet wtedy rządzą nią ustawienia prywatności analityki Twojej przestrzeni: wymiary, które wyłączyłeś, nie pojawiają się w żadnym wierszu, filtr botów obowiązuje, a pseudonimowy hash odwiedzającego stojący za „unikalnymi skanowaniami” nigdy nie jest ujawniany. Odpowiedź wypisuje allowedDimensions, żebyś wiedział, co dostajesz.

scan.aggregate_ready mówi, że agregat jest gotowy; nie zawiera samych skanowań. Jeśli Twoja integracja naprawdę potrzebuje pojedynczych skanowań w czasie rzeczywistym, jest to dostępne na życzenie — porozmawiaj z nami zamiast zakładać, że już istnieje.

Jeśli nie możesz odbierać webhooków

Odpytuj. GET /qr?updatedSince=… zwraca zmienione kody, a GET /scans?since=… przechodzi historię skanowań do przodu ze stabilnym kursorem.