सामग्री पर जाएँ

Mosaqo webhooks

आप जिस भी इवेंट की सदस्यता लेते हैं, उसके लिए हम आपके endpoint पर हस्ताक्षरित JSON बॉडी POST करते हैं, और जब तक आप इसे स्वीकार नहीं करते तब तक बढ़ते अंतराल के साथ दोबारा कोशिश करते हैं।

सदस्यता लेना

या तो Bulk & API में एक endpoint जोड़ें, या webhooks:write key से अपने कोड से ही रजिस्टर करें — ऑटोमेशन प्लेटफ़ॉर्म इसी तरह ट्रिगर चालू और बंद करते हैं:

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

रिस्पॉन्स में हस्ताक्षर करने वाला secret होता है। यह केवल एक बार दिखता है — इसे सहेज लें, या बाद में सब्सक्रिप्शन बदल दें।

इवेंट

इवेंटकब ट्रिगर होता है
qr.createdऐप में या API से कोई कोड बनता है
qr.publishedकोड लाइव होता है और उसका रीडायरेक्ट काम करने लगता है
qr.updatedकंटेंट, डिज़ाइन, नाम या स्थिति बदलती है
qr.archivedकोड आर्काइव होता है और रीडायरेक्ट होस्ट से हट जाता है
qr.deletedकोड स्थायी रूप से डिलीट होता है
redirect.rule_changedकोई गंतव्य या टार्गेटिंग नियम बदलता है
scan.aggregate_readyस्कैन एग्रीगेट तैयार हो जाता है
export.readyकोई असिंक्रोनस एक्सपोर्ट पूरा होता है
approval.changedकोई समीक्षा स्वीकृत या अस्वीकृत होती है
bulk.completedकोई बल्क जॉब पूरा होता है

पेलोड

जब इवेंट किसी QR कोड से जुड़ा हो, तो data.qr में वही सारांश आता है जो GET /qr/{id} लौटाता है — यानी ट्रिगर को अभी-अभी बताए गए रिकॉर्ड के लिए दोबारा कॉल नहीं करनी पड़ती।

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

डिलीवरी सत्यापित करना

हर अनुरोध के साथ एक टाइमस्टैम्प और टाइमस्टैम्प तथा बॉडी दोनों पर बना सिग्नेचर आता है:

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

जिसका टाइमस्टैम्प आपकी अपनी घड़ी से बहुत दूर हो, उसे अस्वीकार करना ही पकड़ी गई डिलीवरी को दोबारा भेजे जाने से रोकता है, इसलिए यह जाँच न छोड़ें। सिग्नेचर की तुलना स्थिर समय में करें, और रॉ रिक्वेस्ट बॉडी इस्तेमाल करें — पार्स किए गए JSON को दोबारा सीरियलाइज़ करने पर बाइट बदल जाते हैं और सिग्नेचर मेल नहीं खाएगा।

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

पुनः प्रयास और विफलताएँ

इवेंट सहेजते ही किसी भी 2xx से जवाब दे दें; काम बाद में करें। हम एक्सपोनेंशियल बैकऑफ़ के साथ छह बार तक दोबारा भेजते हैं, इसलिए धीमा हैंडलर डुप्लिकेट डिलीवरी में बदल जाता है — अपनी तरफ़ X-Mosaqo-Delivery को इडेम्पोटेंसी key की तरह इस्तेमाल करें।

Endpoint सार्वजनिक होस्ट पर HTTPS होने चाहिए। सब्सक्रिप्शन बनाते समय ही लोकल और निजी पते अस्वीकार कर दिए जाते हैं।

अप्रचलित सिग्नेचर

X-Mosaqo-Signature-V0 में पुराना सिग्नेचर होता है जो सिर्फ़ बॉडी पर बना होता है। यह रीप्ले सुरक्षा नहीं दे सकता, मौजूदा रिसीवर्स के माइग्रेट करने के लिए एक रिलीज़ तक ही चलेगा, और किसी नई चीज़ के लिए इस्तेमाल नहीं होना चाहिए।

स्कैन डेटा पुश नहीं किया जाता

जानबूझकर प्रति-स्कैन कोई webhook नहीं है। Mosaqo स्कैन-स्तर की एनालिटिक्स तीसरे पक्ष के endpoint पर नहीं भेजता, और ऊपर की तालिका में कोई भी इवेंट उसे नहीं ले जाता।

स्कैन इतिहास सिर्फ़ अपनी key से खुद लाकर मिलता है — GET /scans — और तब भी उस पर आपके workspace की एनालिटिक्स गोपनीयता सेटिंग लागू होती हैं: जो डाइमेंशन आपने बंद किए हैं वे हर पंक्ति से हटा दिए जाते हैं, बॉट फ़िल्टर लागू होता है, और “अद्वितीय स्कैन” के पीछे का छद्मनाम विज़िटर hash कभी उजागर नहीं होता। रिस्पॉन्स में allowedDimensions सूचीबद्ध होते हैं ताकि आपको पता रहे कि आपको क्या मिल रहा है।

scan.aggregate_ready सिर्फ़ बताता है कि रोलअप तैयार है; इसमें अंतर्निहित स्कैन नहीं होते। अगर आपके इंटीग्रेशन को वाकई अलग-अलग स्कैन रीयल टाइम में चाहिए, तो यह अनुरोध पर उपलब्ध है — इसे मान लेने के बजाय हमसे बात करें।

अगर आप webhook नहीं ले सकते

तो पोल करें। GET /qr?updatedSince=… बदले हुए कोड देता है, और GET /scans?since=… स्थिर cursor के साथ स्कैन इतिहास आगे बढ़ाता है।