Trigger Klaviyo Flows from real-time MailOdds intent
Send MailOdds intent.hot_lead webhooks straight into Klaviyo as Custom Events, so a Flow fires while the visitor is still on the page. HMAC-verified, ready in under an hour.
Prerequisites
- MailOdds account (free tier works)
- API key from dashboard
How the connection works
MailOdds emits an intent.hot_lead webhook when a visitor's heat score crosses your store's hot threshold.
The payload is HMAC-signed with the secret you set when you created the webhook in /dashboard/storefront. Your listener verifies the signature, then forwards the
event to Klaviyo's POST /api/events/ endpoint as a Custom Event named MailOdds Hot Lead. From there, Klaviyo's Flow Builder treats it like any other metric trigger.
Two listener languages are shown below. Pick the stack you already deploy. The Klaviyo Flow at the bottom is the same regardless of language.
Sample webhook payload
This is what MailOdds POSTs to the URL you configure. Note the storefront_context sub-object: it
carries the visitor's last product view, cart value, and UTM trail so your Flow can personalise the followup
without a separate Klaviyo profile lookup.
POST your-webhook-url
JSON{
"event": "intent.hot_lead",
"request_id": "evt_b7f3e8a1c9d24f06",
"timestamp": 1714670400,
"data": {
"base_id": "msg_a3f8c2",
"heat_score": 0.87,
"dwell_seconds": 142,
"environment": "production",
"signal_integrity": {
"score": 0.93,
"probes_correlated": 3
},
"storefront_context": {
"last_product_viewed": {
"id": "sku-1042",
"title": "Alpaca Runner",
"price": "129.00",
"currency": "EUR",
"image_url": "https://shop.alpacanica.com/assets/runner-1.webp"
},
"cart_value": "129.00",
"cart_items": 1,
"session_dwell_seconds": 142,
"page_type": "product",
"utm_source": "klaviyo",
"utm_campaign": "spring_drop"
},
"sms_eligible": true,
"cross_device": false,
"device_count": 1,
"device_types": ["desktop"],
"most_active_device": "desktop",
"first_device_seen_at": "2026-04-29T10:14:02Z",
"cluster_confidence": 1.0
}
} Verify the signature (TypeScript)
The X-Signature-SHA256 header is HMAC-SHA256 of the raw request body, keyed with the secret you set in
the dashboard. Compare with constant time. Reject any mismatch with HTTP 401 so MailOdds retries the delivery on
the next backoff window.
webhook-listener.ts
TYPESCRIPT// Verify the X-Signature-SHA256 header on every inbound webhook.
// Reject any request whose computed HMAC does not match.
//
// Klaviyo's "Receive a Webhook" custom integration ingests this in
// a Cloud Function or Vercel Edge Function fronting your Klaviyo
// API endpoint.
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.MAILODDS_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const rawBody = await req.text();
const signature = req.headers.get("x-signature-sha256") ?? "";
const expected = createHmac("sha256", SECRET)
.update(rawBody)
.digest("hex");
const ok =
signature.length === expected.length &&
timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
if (!ok) {
return new Response("invalid signature", { status: 401 });
}
const payload = JSON.parse(rawBody);
// Forward into Klaviyo as a custom event
await fetch("https://a.klaviyo.com/api/events/", {
method: "POST",
headers: {
"Authorization": `Klaviyo-API-Key undefined`,
"Content-Type": "application/json",
"revision": "2026-01-15"
},
body: JSON.stringify({
data: {
type: "event",
attributes: {
metric: { data: { type: "metric", attributes: { name: "MailOdds Hot Lead" } } },
properties: payload.data,
unique_id: payload.request_id,
time: new Date(payload.timestamp * 1000).toISOString(),
profile: {
data: {
type: "profile",
attributes: { _kx: payload.data.storefront_context?.utm_source }
}
}
}
}
})
});
return new Response("ok", { status: 200 });
} Verify the signature (PHP)
webhook-listener.php
PHP<?php
// PHP verification snippet for the same Klaviyo webhook listener.
// Drop this in a Laravel route, WordPress plugin endpoint, or any
// PSR-7 controller.
$secret = getenv('MAILODDS_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE_SHA256'] ?? '';
$expected = hash_hmac('sha256', $rawBody, $secret);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
echo 'invalid signature';
exit;
}
$payload = json_decode($rawBody, true);
$ch = curl_init('https://a.klaviyo.com/api/events/');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Klaviyo-API-Key ' . getenv('KLAVIYO_API_KEY'),
'Content-Type: application/json',
'revision: 2026-01-15',
],
CURLOPT_POSTFIELDS => json_encode([
'data' => [
'type' => 'event',
'attributes' => [
'metric' => ['data' => ['type' => 'metric', 'attributes' => ['name' => 'MailOdds Hot Lead']]],
'properties' => $payload['data'],
'unique_id' => $payload['request_id'],
'time' => date('c', $payload['timestamp']),
],
],
]),
]);
curl_exec($ch);
curl_close($ch);
http_response_code(200);
echo 'ok';
Klaviyo Flow that consumes the event
Build this in Klaviyo's Flow Builder under Flows -> Create Flow -> From Scratch. Pick the MailOdds Hot Lead metric as the trigger. Add a 15-minute delay, an email, and an optional SMS step
gated on the sms_eligible field MailOdds emits.
flow-definition.json
JSON{
"name": "MailOdds Hot Lead - Personalized Followup",
"trigger": {
"type": "metric",
"metric_name": "MailOdds Hot Lead"
},
"filter": {
"and": [
{ "property": "data.heat_score", "op": ">=", "value": 0.8 },
{ "property": "data.storefront_context.cart_value", "op": ">", "value": 0 }
]
},
"delay_minutes": 15,
"actions": [
{
"type": "email",
"template_id": "TEMPLATE_HOT_LEAD_RECOVERY",
"subject": "{{ event.data.storefront_context.last_product_viewed.title }} is still in your cart",
"personalisation": {
"product_image": "{{ event.data.storefront_context.last_product_viewed.image_url }}",
"product_price": "{{ event.data.storefront_context.last_product_viewed.price }}",
"currency": "{{ event.data.storefront_context.last_product_viewed.currency }}"
}
},
{
"type": "sms",
"condition": "{{ event.data.sms_eligible == true }}",
"delay_minutes": 60,
"body": "Still thinking about {{ event.data.storefront_context.last_product_viewed.title }}? Free EU shipping today only."
}
]
} The JSON above is a description of the steps. Klaviyo does not import Flow JSON directly; recreate the steps in
the Flow Builder UI. The trigger metric name (MailOdds Hot Lead) must match the metric.attributes.name your listener posts.
Operational notes
- Idempotency: always pass
payload.request_idas Klaviyo'sunique_id. MailOdds retries on 5xx; Klaviyo dedupes. - Retries: MailOdds retries 3 times after the initial attempt (5s, 30s, 120s). After 4 total failures, the webhook is auto-disabled and a dashboard alert fires.
- Cross-device: when
cross_device=true, the visitor was first seen on one device and converted on another. Usemost_active_deviceto pick the channel (push the SMS to the mobile, the email to the desktop). - Plan gating: Klaviyo Custom Events need the Klaviyo Pro plan. The MailOdds intent webhook is on Growth+.