Aller au contenu

Webhooks Mosaqo

Nous envoyons un POST au corps JSON signé vers votre endpoint pour chaque événement auquel vous êtes abonné, en réessayant avec un délai croissant jusqu’à ce que vous l’acceptiez.

S’abonner

Ajoutez un endpoint dans Bulk & API, ou enregistrez-en un depuis votre propre code avec une clé webhooks:write — c’est ainsi qu’une plateforme d’automatisation active et désactive un déclencheur :

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 réponse contient le secret de signature. Il n’est affiché qu’une fois — conservez-le, ou remplacez l’abonnement plus tard.

Événements

ÉvénementSe déclenche quand
qr.createdUn code est créé, dans l’application ou via l’API
qr.publishedUn code passe en ligne et sa redirection commence à répondre
qr.updatedLe contenu, le design, le nom ou le statut change
qr.archivedUn code est archivé et retiré de l’hôte de redirection
qr.deletedUn code est supprimé définitivement
redirect.rule_changedUne destination ou une règle de ciblage change
scan.aggregate_readyUne agrégation de scans est prête
export.readyUn export asynchrone est terminé
approval.changedUne relecture est approuvée ou rejetée
bulk.completedUne tâche en lot se termine

Payload

Quand un événement concerne un QR code, data.qr porte le même résumé que celui renvoyé par GET /qr/{id} — un déclencheur n’a donc pas à redemander la fiche dont on vient de lui parler.

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

Vérifier une livraison

Chaque requête porte un horodatage et une signature couvrant à la fois l’horodatage et le corps :

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

Rejeter tout ce dont l’horodatage s’éloigne de votre propre horloge est précisément ce qui rend une livraison interceptée non rejouable : ne sautez pas cette vérification. Comparez les signatures en temps constant et utilisez le corps brut de la requête — resérialiser le JSON analysé change les octets, et la signature ne correspondra plus.

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

Nouvelles tentatives et échecs

Répondez par n’importe quel 2xx dès que vous avez stocké l’événement, et faites le travail ensuite. Nous réessayons jusqu’à six fois avec un délai exponentiel : un traitement lent se traduit donc par des livraisons en double — traitez X-Mosaqo-Delivery comme une clé d’idempotence de votre côté.

Les endpoints doivent être en HTTPS sur un hôte public. Les adresses locales et privées sont refusées dès la création de l’abonnement.

Signature dépréciée

X-Mosaqo-Signature-V0 porte une signature plus ancienne calculée sur le seul corps. Elle ne peut exprimer aucune protection contre le rejeu, reste présente une version le temps que les récepteurs existants migrent, et ne doit servir à rien de nouveau.

Les données de scan ne sont pas poussées

Il n’existe délibérément aucun webhook par scan. Mosaqo n’envoie pas de statistiques au niveau du scan vers des endpoints tiers, et aucun événement du tableau ci-dessus n’en transporte.

L’historique des scans n’est accessible qu’en le récupérant vous-même avec votre propre clé — GET /scans — et il reste alors régi par les réglages de confidentialité analytique de votre espace : les dimensions que vous avez désactivées sont absentes de chaque ligne, le filtre anti-bots s’applique, et le hachage pseudonyme du visiteur derrière les « scans uniques » n’est jamais exposé. La réponse liste allowedDimensions pour que vous sachiez ce que vous obtenez.

scan.aggregate_ready signale qu’une agrégation est prête ; il ne contient pas les scans sous-jacents. Si votre intégration a réellement besoin des scans individuels en temps réel, c’est possible sur demande — parlez-nous-en plutôt que de le supposer acquis.

Si vous ne pouvez pas recevoir de webhooks

Interrogez l’API. GET /qr?updatedSince=… donne les codes modifiés, et GET /scans?since=… parcourt l’historique des scans avec un curseur stable.