What you’ll build
This guide shows you how to:
- Receive Sahha webhooks for
ArchetypeCreatedIntegrationEvent. - Verify the webhook signature (
X-Signature) and extract the user identifier (X-External-Id). - Map Sahha archetypes into flat, segmentable Klaviyo profile properties.
- Upsert those properties to Klaviyo via Create or Update Profile.
- (Optional) Emit a Klaviyo event so archetype changes can directly trigger flows.
This is the recommended approach when you want to use Sahha as a behavioral segmentation input for Klaviyo campaigns/flows (rather than treating archetypes as app-only state).
Architecture
Sahha → (Webhook) → Your Integration Service → Klaviyo
- Sahha sends a webhook request with:
X-Signature(HMAC-SHA256 of the raw payload using your secret)X-External-Id(your user key in Sahha)X-Event-Type(e.g.ArchetypeCreatedIntegrationEvent)
- Your service:
- Verifies signature
- Maps
externalId→ email/phone (recommended) for Klaviyo - Upserts profile properties in Klaviyo
- Optionally creates an event (
Sahha Archetype Assigned) for flow triggers
Important webhook behavior: Sahha waits up to 30 seconds for a response, retries failures up to 5 times, and events may arrive out of order—use timestamps/IDs for sequencing.
(Your handler should ACK fast, and do downstream work asynchronously.)
See: Sahha webhook behavior + example handler.
Prerequisites
Sahha
- Webhooks enabled in your Sahha dashboard
- A webhook endpoint URL you control
- Your
SAHHA_WEBHOOK_SECRET(used to verifyX-Signature) - Archetypes enabled for your account/product
- You are receiving
ArchetypeCreatedIntegrationEventpayloads
References:
- https://docs.sahha.ai/docs/connect/webhooks
- https://docs.sahha.ai/docs/connect/webhooks/events
- https://docs.sahha.ai/docs/products/archetypes
- https://sahha.ai/archetypes-api
Klaviyo
- A Klaviyo Private API Key
- A pinned Klaviyo API
revisionheader (date-based API versions) - Familiarity with Klaviyo profile properties + segmentation
References:
- https://developers.klaviyo.com/en/reference/profiles_api_overview
- https://developers.klaviyo.com/en/docs/api_versioning_and_deprecation_policy
- https://developers.klaviyo.com/en/reference/events_api_overview
Step 0 — Choose the identifier strategy (don’t skip this)
Klaviyo supports profile identifiers like email (recommended), phone_number, and external_id.
Recommended: use email as your primary key
If you can map X-External-Id → email in your system, do that. It avoids duplication and aligns with Klaviyo’s own recommendation.
Avoid relying on Klaviyo external_id as your only key
Klaviyo explicitly notes external_id is not involved in profile merging and can lead to duplicate profiles. Use it only if you understand the duplication impact.
Practical approach:
- In Sahha, keep using your own stable
externalId. - In your integration layer, map it to Klaviyo’s recommended identifier:
email(best)- or
phone_number(if you primarily message via SMS)
Step 1 — Decide your Klaviyo property schema (flat + segmentable)
Klaviyo profile properties are great for segmentation, flow filters, and template personalization, but Klaviyo recommends using non-object values for segmentation.
So: store archetype fields as flat strings, not nested objects.
Suggested naming convention
For each archetype event (from Sahha), write:
sahha_archetype_{name}_{periodicity}={value}sahha_archetype_{name}_{periodicity}_created_at={createdAtUtc}sahha_archetype_{name}_{periodicity}_start={startDateTime}sahha_archetype_{name}_{periodicity}_end={endDateTime}sahha_archetype_{name}_{periodicity}_version={version}
Example for chronotype weekly:
sahha_archetype_chronotype_weekly = early_bird
This gives you predictable targeting like:
- Segment:
sahha_archetype_chronotype_weekly equals "night_owl" - Flow filter:
sahha_archetype_activity_level_daily equals "highly_active"
Step 2 — Receive and verify Sahha webhooks
Sahha signs each webhook. You should verify:
- Read the raw request body (not JSON-parsed yet).
- Compute HMAC-SHA256 with
SAHHA_WEBHOOK_SECRET. - Compare to
X-Signature.
Below is a production-ready Node/Express handler that:
- Validates required headers
- Uses a timing-safe compare
- ACKs fast (2xx)
- Processes downstream work after ACK
Notes:
express.text({ type: "*/*" })is used so you can compute HMAC over the raw payload exactly as received.- You can replace the in-memory dedupe with Redis / DB depending on your scale.
import express from "express";
import crypto from "crypto";
const app = express();
// IMPORTANT: capture RAW body for signature verification
app.use(express.text({ type: "*/*" }));
const SAHHA_WEBHOOK_SECRET = process.env.SAHHA_WEBHOOK_SECRET!;
const KLAVIYO_PRIVATE_API_KEY = process.env.KLAVIYO_PRIVATE_API_KEY!;
const KLAVIYO_REVISION = process.env.KLAVIYO_REVISION ?? "2026-01-15"; // pin to a valid revision
if (!SAHHA_WEBHOOK_SECRET) throw new Error("Missing SAHHA_WEBHOOK_SECRET");
if (!KLAVIYO_PRIVATE_API_KEY) throw new Error("Missing KLAVIYO_PRIVATE_API_KEY");
// Optional: basic in-memory dedupe to reduce duplicate work during retries.
// Replace with persistent storage for real deployments.
const processedArchetypeIds = new Set<string>();
app.post("/webhooks/sahha", async (req, res) => {
const signature = req.get("X-Signature") ?? "";
const externalId = req.get("X-External-Id") ?? "";
const eventType = req.get("X-Event-Type") ?? "";
if (!signature || !externalId || !eventType) {
return res.status(400).json({ error: "Missing required headers" });
}
// Verify signature against RAW body
const computed = crypto
.createHmac("sha256", SAHHA_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const sigBuf = Buffer.from(signature, "hex");
const cmpBuf = Buffer.from(computed, "hex");
// timingSafeEqual throws if lengths differ
if (sigBuf.length !== cmpBuf.length || !crypto.timingSafeEqual(sigBuf, cmpBuf)) {
return res.status(401).json({ error: "Invalid signature" });
}
// ACK fast (Sahha timeout is 30s; do heavy work async)
res.status(200).json({ received: true });
try {
if (eventType !== "ArchetypeCreatedIntegrationEvent") return;
const payload = JSON.parse(req.body) as SahhaArchetypeCreatedIntegrationEvent;
// Dedupe (optional)
if (processedArchetypeIds.has(payload.id)) return;
processedArchetypeIds.add(payload.id);
// Map externalId -> email (recommended)
const email = await lookupEmailByExternalId(externalId);
if (!email) {
console.warn("No email mapping for externalId:", externalId);
return;
}
const props = mapArchetypeToKlaviyoProperties(payload);
// 1) Update Klaviyo profile properties
await klaviyoCreateOrUpdateProfile({
email,
properties: props,
apiKey: KLAVIYO_PRIVATE_API_KEY,
revision: KLAVIYO_REVISION,
});
// 2) Optional: emit an event so flows can trigger immediately
await klaviyoCreateEvent({
email,
eventName: "Sahha Archetype Assigned",
uniqueId: payload.id,
eventProperties: {
sahha_archetype_name: payload.name,
sahha_archetype_value: payload.value,
sahha_archetype_periodicity: payload.periodicity,
sahha_archetype_created_at: payload.createdAtUtc,
sahha_archetype_start: payload.startDateTime,
sahha_archetype_end: payload.endDateTime,
sahha_archetype_version: payload.version,
},
profileProperties: props,
apiKey: KLAVIYO_PRIVATE_API_KEY,
revision: KLAVIYO_REVISION,
});
} catch (err) {
// Since we already ACKed, implement your own retry strategy here:
// queue, DLQ, exponential backoff, etc.
console.error("Error processing Sahha webhook:", err);
}
});
app.listen(3000, () => console.log("Listening on :3000"));
/** ===== Types (matches Sahha ArchetypeCreatedIntegrationEvent) ===== */
type SahhaArchetypeCreatedIntegrationEvent = {
id: string;
profileId: string;
accountId: string;
externalId: string;
name: string; // e.g. chronotype, activity_level, sleep_pattern
value: string; // e.g. early_bird, night_owl, highly_active
dataType: string; // e.g. string
periodicity: string; // daily | weekly | monthly
ordinality: number; // 0-based
startDateTime: string;
endDateTime: string;
createdAtUtc: string;
version: number;
};
/** ===== Your app’s user lookup ===== */
async function lookupEmailByExternalId(externalId: string): Promise<string | null> {
// Replace with your DB lookup.
// Example: SELECT email FROM users WHERE external_id = $1
return null;
}
/** ===== Property mapping ===== */
function mapArchetypeToKlaviyoProperties(a: SahhaArchetypeCreatedIntegrationEvent): Record<string, string | number> {
const keyBase = `sahha_archetype_${a.name}_${a.periodicity}`;
// Keep values flat for segmentation/flow filtering
return {
[keyBase]: a.value,
[`${keyBase}_created_at`]: a.createdAtUtc,
[`${keyBase}_start`]: a.startDateTime,
[`${keyBase}_end`]: a.endDateTime,
[`${keyBase}_version`]: a.version,
};
}
Step 3 — Upsert profile properties in Klaviyo
Klaviyo supports “Create or Update Profile” with a payload shaped like:
{
"data": {
"type": "profile",
"attributes": { "email": "..." },
"properties": { "your_property": "value" }
}
}
Example: cURL request
curl --request POST \
--url "https://a.klaviyo.com/api/profile-import/" \
--header "Authorization: Klaviyo-API-Key $KLAVIYO_PRIVATE_API_KEY" \
--header "accept: application/vnd.api+json" \
--header "content-type: application/vnd.api+json" \
--header "revision: 2026-01-15" \
--data '{
"data": {
"type": "profile",
"attributes": {
"email": "user@example.com"
},
"properties": {
"sahha_archetype_chronotype_weekly": "early_bird",
"sahha_archetype_chronotype_weekly_created_at": "2025-01-22T06:00:00Z",
"sahha_archetype_chronotype_weekly_start": "2025-01-15T00:00:00Z",
"sahha_archetype_chronotype_weekly_end": "2025-01-22T00:00:00Z",
"sahha_archetype_chronotype_weekly_version": 1
}
}
}'
Node helper used in the webhook example
async function klaviyoCreateOrUpdateProfile(args: {
email: string;
properties: Record<string, string | number>;
apiKey: string;
revision: string;
}) {
const res = await fetch("https://a.klaviyo.com/api/profile-import/", {
method: "POST",
headers: {
Authorization: `Klaviyo-API-Key ${args.apiKey}`,
accept: "application/vnd.api+json",
"content-type": "application/vnd.api+json",
revision: args.revision,
},
body: JSON.stringify({
data: {
type: "profile",
attributes: { email: args.email },
properties: args.properties,
},
}),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Klaviyo profile upsert failed: ${res.status} ${text}`);
}
}
Consent note: Klaviyo’s Profiles API distinguishes between updating properties vs subscribing a profile. You can’t subscribe and edit custom properties in the same API request—do subscription first, then a separate call to update properties if needed.
Step 4 (Optional) — Emit a Klaviyo event to trigger flows
Profile properties are perfect for segmentation and template logic.
But for real-time orchestration (e.g., “when archetype changes, start a flow”), it’s often better to also send an event.
Why send an event?
- A flow can be triggered by an event like “Sahha Archetype Assigned”
- You can still filter the flow by profile properties, but the trigger becomes deterministic
- You can use
unique_idto reduce duplicates
Event API request
async function klaviyoCreateEvent(args: {
email: string;
eventName: string;
uniqueId: string;
eventProperties: Record<string, string | number>;
profileProperties: Record<string, string | number>;
apiKey: string;
revision: string;
}) {
const res = await fetch("https://a.klaviyo.com/api/events/", {
method: "POST",
headers: {
Authorization: `Klaviyo-API-Key ${args.apiKey}`,
accept: "application/vnd.api+json",
"content-type": "application/vnd.api+json",
revision: args.revision,
},
body: JSON.stringify({
data: {
type: "event",
attributes: {
properties: args.eventProperties,
metric: {
data: {
type: "metric",
attributes: { name: args.eventName },
},
},
profile: {
data: {
type: "profile",
attributes: { email: args.email },
properties: args.profileProperties,
},
},
unique_id: args.uniqueId,
},
},
}),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Klaviyo event create failed: ${res.status} ${text}`);
}
}
Step 5 — Validate everything in Klaviyo (end-to-end checklist)
A) Confirm profile properties are being written
- In Klaviyo, search for the user profile (by email).
- Check Properties for keys like:
sahha_archetype_chronotype_weeklysahha_archetype_activity_level_daily
- Confirm values are strings/numbers (not nested objects), so they’re usable for segmentation.
B) Build a segment using an archetype
Create a segment like:
- Condition:
Properties about someonesahha_archetype_chronotype_weeklyequalsnight_owl
This segment should automatically grow/shrink as Sahha archetypes update.
C) Trigger a flow from the event (optional path)
If you implemented the event:
- Go to Metrics
- Confirm you see a metric like:
Sahha Archetype Assigned
- Create a Flow triggered by that metric.
- Add a Flow Filter:
sahha_archetype_chronotype_weekly equals "early_bird"
- Add messaging logic (email/SMS/push) targeted to that cohort.
Step 6 — Production hardening (the parts that prevent “it worked once”)
1) Handle webhook retries properly
Sahha retries failed deliveries (up to 5 times). If you ACK immediately but downstream work fails, implement your own retry queue (e.g. SQS, BullMQ, Cloud Tasks).
2) Idempotency & out-of-order safety
Sahha events can arrive out of order. Use:
payload.idas a unique record keycreatedAtUtcto reason about freshness- optional dedupe storage (Redis/DB) to prevent reprocessing
3) Pin a Klaviyo revision header
Klaviyo’s API versions are date-based and passed via the revision header. Pin a revision so your integration doesn’t break unexpectedly.
4) Keep archetype data “light”
For CX targeting, archetypes are usually enough. Avoid pushing raw samples/scores unless you have a specific CX use case and clear consent posture.
Common gotchas
“My segment doesn’t work”
Most often this is because you stored an object/array as a property value. Keep segmentation keys as flat strings/numbers.
“I’m seeing duplicate profiles”
If you used external_id without email/phone, duplication is more likely. Use email as the primary identifier whenever possible.
“Webhook signature verification fails”
Make sure you’re hashing the raw body, not a JSON-stringified representation (whitespace/ordering changes will break the signature).
References
Sahha
- https://docs.sahha.ai/docs/connect/webhooks
- https://docs.sahha.ai/docs/connect/webhooks/events
- https://docs.sahha.ai/docs/products/archetypes
- https://sahha.ai/archetypes-api