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
- Open Dashboard > Settings > Webhooks (owners and admins only).
- Choose Create Webhook, enter an HTTPS endpoint URL and pick the events.
- Copy the signing secret. It is shown once; you can rotate it later.
- Use the send button to deliver a
webhook.testevent 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
| Event | Sent when |
|---|---|
rewrite.generated | GetIntent 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.started | An A/B test moved to running (started or resumed). |
experiment.winner_declared | Auto-optimize found a statistically significant winner. Includes the winning variant, its conversion rate, the improvement over control and the stats for every variant. |
experiment.completed | An A/B test was completed, manually or by auto-optimize. |
conversion | The pixel (getintentTrackConversion) or the analytics API recorded a conversion. Includes page URL, UTM parameters, the variant (when known) and your metadata. |
visitor.converted | A conversion was attributed to the A/B test variant the visitor was served (first conversion per visitor and variant). |
site.created | A site was added in the dashboard. |
site.updated | Site 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-IdandIdempotency-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-Afterheader 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
createdAtto order.