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
feedback.response_receivedKtoś odpowiada na formularz feedbacku
feedback.low_score_receivedOcena spada poniżej progu niskiej oceny
feedback.response_resolvedOdpowiedź zostaje oznaczona jako obsłużona w skrzynce
feedback.campaign_publishedMiejsce feedbacku wchodzi na żywo

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

Zdarzenia feedbacku

Moduł feedbacku obejmują cztery zdarzenia i to cała jego powierzchnia integracyjna. Każda dostawa nosi identyfikatory, ocenę i jej rodzaj, nigdy tego, co ktoś napisał: treść webhooka trafia tam, gdzie zapisze ją odbiorca, a to nie decyzja, którą przestrzeń robocza podejmuje za swojego klienta.

{
  "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 wychodzi tylko, gdy w przestrzeni roboczej włączone jest powiadomienie o niskiej ocenie, i tylko poniżej ustawionego tam progu. feedback.response_received wychodzi i tak przy każdej odpowiedzi — subskrybuj je, jeśli chcesz wszystkie.

Nie ma endpointu na klucz API dla odpowiedzi. Moduł czyta je trasą uwierzytelnianą sesją, do której klucz nie dociera, więc same odpowiedzi wychodzą ze skrzynki w CSV — napisz do nas, zamiast zakładać, że istnieje trasa do pobierania.

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.