ENGINEERING // EVENT_ARCHITECTURE

    WEBHOOK SYSTEM.

    Push-based event delivery with signature verification, exponential backoff retries and event type filtering.

    Live in the product. A workspace admin adds endpoints under Webhooks, picks the events to receive and copies the signing secret once. Every attempt is logged with its response and retried on a backing-off schedule.

    EVENT_CATALOG

    EVENT TYPES

    EVENTCATEGORYDESCRIPTION
    pixel.events.receivedATTRIBUTIONA batch of server-side events was accepted by POST /api/v1/events
    actuals.importedPLANNINGDelivered performance rows were written to a media plan
    api_key.createdACCOUNTA workspace admin issued a new API key
    api_key.revokedACCOUNTA workspace admin revoked an API key
    webhook.testSYSTEMSent when someone presses Send test on an endpoint
    sandbox.pingSANDBOXSandbox only. A harmless payload for wiring up a receiver

    PAYLOAD_SCHEMA

    EVENT PAYLOAD

    POST https://your-endpoint.com/webhooks
    {
      "id": "8f2b1c34-5d6e-4f80-9a1b-2c3d4e5f6071",
      "type": "pixel.events.received",
      "event_id": "1d9a7c22-0b3e-4a11-9f5c-77c2f0b41f9a",
      "created_at": "2026-09-12T07:00:00Z",
      "data": {
        "pixel_id": "0a6c1f2e-7b44-4c9a-8f31-19ad2c7e5b60",
        "accepted": 12,
        "duplicates": 1,
        "consent_blocked": 0,
        "received": 13
      }
    }

    INTEGRITY_VERIFICATION

    SIGNATURE VERIFICATION

    HMAC-SHA256 Signatures

    Every delivery carries a QN-Signature header in the form t=<unix seconds>,v1=<hex>. The hash covers the string <t>.<raw body> using your endpoint's signing secret. Reject a timestamp older than five minutes and compare in constant time.

    Verification Example (Node.js)
    import crypto from 'crypto';
    
    function verifyWebhook(rawBody, header, secret) {
      const parts = Object.fromEntries(
        header.split(',').map((part) => part.split('='))
      );
      const age = Math.abs(Date.now() / 1000 - Number(parts.t));
      if (!Number.isFinite(age) || age > 300) return false;
    
      const expected = crypto
        .createHmac('sha256', secret)
        .update(parts.t + '.' + rawBody)
        .digest('hex');
    
      return crypto.timingSafeEqual(
        Buffer.from(parts.v1, 'hex'),
        Buffer.from(expected, 'hex')
      );
    }

    DELIVERY_ASSURANCE

    RETRY POLICY

    Only a timeout, a network failure, 408, 425, 429 or a 5xx is retried, on the schedule below with jitter. A Retry-After header on a 429 or 503 is honoured, capped at 24 hours. Anything else, such as 400 or 404, is a permanent failure and goes straight to the dead-letter list instead of being hammered. After the final attempt the delivery is dead-lettered, and an endpoint that fails 20 times in a row is switched off until an admin re-enables it. Dead-lettered deliveries stay in the inspector and can be replayed once your receiver is healthy: the original event id is preserved so your own deduplication still recognises it.

    ATTEMPTDELAYTIMEOUT
    1stImmediate10s
    2nd1 minute10s
    3rd5 minutes10s
    4th30 minutes10s
    5th2 hours10s
    Final6 hours10s

    INBOUND

    RECEIVING FROM YOUR SYSTEMS

    Webhooks run both ways. A workspace admin can create a receiver, which returns a private address under /api/public/inbound/ and a shared secret. The sending system signs each request with the same scheme we use outbound: QN-Signature: t=<unix>,v1=<hmac sha256>over <t>.<body>.

    Requests signed more than five minutes ago are refused, bodies are capped at 256 KB, and messages are de-duplicated on the sender’s event id, so a retry is acknowledged rather than stored twice. Accepted messages return 202 and are routed immediately.

    Routing reads a type field. Today one type is acted on:actuals.reported, whose data carries amedia_plan_id and up to 500 daily delivery rows, written on the same (plan, line, date) key as the actuals import, so re-sending a day restates it. The response reports processing asprocessed, ignored (valid but nothing to act on) orfailed (understood, write did not land, retried on next traffic). Any other type is stored and left untouched.

    Rotating a receiver’s secret works the same way as rotating ours: the retiring secret keeps being accepted for 24 hours, so the sending system can swap credentials without losing a message. Any stored message can be run through processing again from the workspace, exactly as an outbound delivery can be replayed.

    KEY_LIFECYCLE

    ROTATING A SIGNING SECRET

    Rotation issues a new secret and keeps the old one valid for an overlap window, 72 hours by default and adjustable between 1 and 168 hours. During the overlap every delivery is signed twice and both signatures travel in the same header as two v1= parts. A receiver that accepts any matching part keeps working while you deploy the new secret, with no dropped deliveries and no maintenance window. Once the new secret is live everywhere, end the overlap early from the endpoint.

    Both SDKs do this for you. Pass an array of secrets tounwrap in the TypeScript client orunwrap_webhook in Python, and either one verifies.

    NETWORK

    FIREWALLS AND ALLOW-LISTS

    Deliveries leave from our hosting provider’s edge network, so the source address is not a fixed list we can publish and promise to keep stable. Pinning a firewall to addresses we cannot guarantee would break your integration the day they change, so we do not ask you to.

    Authenticate the request instead of the address. The signature is the control that matters: it proves the body came from us and has not been altered. On top of it, allow-list our request shape rather than our IPs. Every delivery sends User-Agent: QubitNotion-Webhooks/1 and the headers QN-Signature, QN-Event-Type,QN-Event-Id, QN-Delivery-Id andQN-Attempt. Requests are always HTTPS POST to the exact path you registered, so a hard-to-guess path segment gives you a cheap first filter before the signature check.

    For a WAF rule, allow POST to your receiver path with a JSON body up to 256 KB and the QN-Signature header present, then drop everything else. Do not rate-limit below roughly one request per second per endpoint, and answer with 2xx as soon as you have stored the payload: a receiver that works before responding will hit the 10 second timeout and earn itself retries. If your security policy requires source pinning, contact us and we will confirm what the current edge ranges are at that moment, with the caveat that they can change.

    RETURN TO ENGINEERING TERMINAL