Webhooks

Receive a signed HTTPS request when GetIntent personalizes a page, an A/B test finds a winner or a visitor converts.

Set up a webhook

  1. Open Dashboard > Settings > Webhooks (owners and admins only).
  2. Choose Create Webhook, enter an HTTPS endpoint URL and pick the events.
  3. Copy the signing secret. It is shown once; you can rotate it later.
  4. Use the send button to deliver a webhook.test event and check your endpoint.

Endpoints must use HTTPS and resolve to a public internet address. Private, loopback, link-local and cloud metadata addresses are refused when you save the URL and again before every delivery. Redirects are not followed.

Events

EventSent when
rewrite.generatedGetIntent generated new personalized copy for a page and visitor context (a cache miss, or a background refresh of stale copy). Includes the original and personalized text of each element, the UTM/signal/intent context, the experiment variant and the identified company. Not sent for repeat views served from cache.
experiment.startedAn A/B test moved to running (started or resumed).
experiment.winner_declaredAuto-optimize found a statistically significant winner. Includes the winning variant, its conversion rate, the improvement over control and the stats for every variant.
experiment.completedAn A/B test was completed, manually or by auto-optimize.
conversionThe pixel (getintentTrackConversion) or the analytics API recorded a conversion. Includes page URL, UTM parameters, the variant (when known) and your metadata.
visitor.convertedA conversion was attributed to the A/B test variant the visitor was served (first conversion per visitor and variant).
site.createdA site was added in the dashboard.
site.updatedSite settings were changed in the dashboard, or the script was detected on the site for the first time. Lists the changed field names, never their values.

conversion and visitor.converted are capped at 300 events per minute per organization. Events above the cap are still counted in Analytics but are not sent as webhooks.

Payload

Every request is a JSON POST with the same envelope. The event-specific fields are in data.

{
  "id": "evt_5f0c1e9a2b7d4c3e8a1f6b20",
  "type": "experiment.winner_declared",
  "apiVersion": "2026-10-25",
  "createdAt": "2026-10-25T09:14:03.221Z",
  "organizationId": "clx0org...",
  "data": {
    "site": { "id": "clx0site...", "name": "Marketing site", "domain": "example.com" },
    "experiment": { "id": "clx0exp...", "name": "Hero headline", "status": "running", "...": "..." },
    "winner": { "id": "clx0var...", "name": "Outcome-led", "isControl": false, "impressions": 1840, "conversions": 97, "conversionRate": 0.0527 },
    "conversionRate": 0.0527,
    "improvementPercent": 31.4,
    "confidenceLevel": 0.95,
    "declaredBy": "auto_optimize",
    "variants": ["..."]
  }
}

Headers

  • GetIntent-Signature: t=<unix seconds>,v1=<hex>: HMAC-SHA256 of <t>.<raw body> with your signing secret.
  • GetIntent-Event: the event type.
  • GetIntent-Event-Id and Idempotency-Key: the event ID. It stays the same on every retry, so use it to ignore duplicates.
  • GetIntent-Delivery-Id: the delivery ID shown in the delivery log.
  • GetIntent-Attempt: 1 for the first attempt, up to 8.

Verify the signature

Compute the HMAC over the raw request body, compare it in constant time with every v1 value, and reject timestamps more than five minutes old. After you rotate the secret, requests carry two v1 values for 24 hours (new and previous secret), so you can switch without dropping events.

Node.js

import crypto from "node:crypto"

// rawBody must be the exact request body string, before JSON.parse.
export function verifyGetIntentSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = header.split(",")
  const t = Number(parts.find((p) => p.startsWith("t="))?.slice(2))
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false

  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest()
  return parts
    .filter((p) => p.startsWith("v1="))
    .some((p) => {
      const given = Buffer.from(p.slice(3), "hex")
      return given.length === expected.length && crypto.timingSafeEqual(given, expected)
    })
}

Python

import hashlib, hmac, time

def verify_getintent_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    items = [p.split("=", 1) for p in header.split(",") if "=" in p]
    t = next((v for k, v in items if k == "t"), None)
    if not t or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(v, expected) for k, v in items if k == "v1")

Retries and the delivery log

  • Respond with any 2xx status within 10 seconds. Anything else counts as a failure.
  • Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours (8 attempts in total). A Retry-After header on a 429 or 503 response is respected, up to 24 hours.
  • After the last attempt the delivery is marked failed. Endpoints that resolve to a blocked address or redirect fail at once.
  • The delivery log (history icon next to each webhook) shows every delivery with its status, HTTP code, attempts, next retry and the first 1 KB of your response. Finished deliveries can be sent again with Redeliver. Entries are kept for 30 days.
  • Deliveries are at least once and can arrive out of order. Use the event ID to deduplicate and createdAt to order.