Naar de inhoud

Mosaqo-webhooks

We sturen voor elke gebeurtenis waarop je bent geabonneerd een POST met ondertekende JSON naar je endpoint, en proberen het met groeiende tussenpozen opnieuw tot je hem accepteert.

Abonneren

Voeg een endpoint toe in Bulk & API, of registreer er een vanuit je eigen code met een webhooks:write-sleutel — zo zet een automatiseringsplatform een trigger aan en uit:

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

Het antwoord bevat het secret voor ondertekening. Het wordt maar één keer getoond — bewaar het, of vervang het abonnement later.

Gebeurtenissen

GebeurtenisWordt geactiveerd wanneer
qr.createdEen code wordt gemaakt, in de app of via de API
qr.publishedEen code gaat live en de omleiding begint te werken
qr.updatedInhoud, ontwerp, naam of status verandert
qr.archivedEen code wordt gearchiveerd en van de omleidingshost gehaald
qr.deletedEen code wordt definitief verwijderd
redirect.rule_changedEen bestemming of targetingregel verandert
scan.aggregate_readyEen scanaggregatie is klaar
export.readyEen asynchrone export is voltooid
approval.changedEen beoordeling wordt goedgekeurd of afgewezen
bulk.completedEen bulkopdracht is afgerond

Payload

Gaat een gebeurtenis over een QR-code, dan draagt data.qr dezelfde samenvatting die GET /qr/{id} teruggeeft — een trigger hoeft het record waarover hij zojuist is ingelicht dus niet nog eens op te vragen.

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

Een levering verifiëren

Elk verzoek draagt een tijdstempel en een handtekening over zowel het tijdstempel als de body:

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

Alles weigeren waarvan het tijdstempel ver van je eigen klok ligt, is precies wat een onderschepte levering onherhaalbaar maakt — sla die controle niet over. Vergelijk handtekeningen in constante tijd en gebruik de ruwe request-body: de geparste JSON opnieuw serialiseren verandert de bytes, en dan klopt de handtekening niet meer.

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

Nieuwe pogingen en fouten

Antwoord met een willekeurige 2xx zodra je de gebeurtenis hebt opgeslagen; doe het werk daarna. We proberen het tot zes keer opnieuw met exponentieel groeiende tussenpozen, dus een trage handler leidt tot dubbele leveringen — behandel X-Mosaqo-Delivery aan jouw kant als idempotentiesleutel.

Endpoints moeten HTTPS zijn op een publieke host. Lokale en private adressen worden al bij het aanmaken van het abonnement geweigerd.

Verouderde handtekening

X-Mosaqo-Signature-V0 draagt een oudere handtekening die alleen over de body is berekend. Die kan geen bescherming tegen herhaling uitdrukken, blijft één release meelopen zodat bestaande ontvangers kunnen migreren, en mag voor niets nieuws worden gebruikt.

Scangegevens worden niet gepusht

Er is bewust geen webhook per scan. Mosaqo stuurt geen analytics op scanniveau naar endpoints van derden, en geen enkele gebeurtenis in de tabel hierboven draagt die.

De scangeschiedenis is alleen beschikbaar door haar zelf op te halen met je eigen sleutel — GET /scans — en zelfs dan wordt ze bepaald door de analytics-privacyinstellingen van je werkruimte: dimensies die je hebt uitgezet ontbreken in elke rij, het botfilter geldt, en de pseudonieme bezoekershash achter “unieke scans” wordt nooit prijsgegeven. Het antwoord toont allowedDimensions zodat je weet wat je krijgt.

scan.aggregate_ready meldt dat een aggregatie klaar is; de onderliggende scans zitten er niet in. Heeft je integratie echt losse scans in realtime nodig, dan kan dat op aanvraag — bespreek het met ons in plaats van ervan uit te gaan dat het er al is.

Als je geen webhooks kunt ontvangen

Poll dan. GET /qr?updatedSince=… geeft gewijzigde codes, en GET /scans?since=… loopt met een stabiele cursor vooruit door de scangeschiedenis.