Updated today Guides

Enrich Klaviyo User Profiles with Sahha Archetypes

A step-by-step tutorial to integrate Sahha archetype outputs into Klaviyo user attributes, enabling smarter segmentation and personalized messaging based on health and lifestyle patterns. This guide covers prerequisites, webhook configuration, attribute mapping strategies, implementation examples, and testing tips for a seamless integration.

What you’ll build

This guide shows you how to:

  1. Receive Sahha webhooks for ArchetypeCreatedIntegrationEvent.
  2. Verify the webhook signature (X-Signature) and extract the user identifier (X-External-Id).
  3. Map Sahha archetypes into flat, segmentable Klaviyo profile properties.
  4. Upsert those properties to Klaviyo via Create or Update Profile.
  5. (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 externalIdemail/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 verify X-Signature)
  • Archetypes enabled for your account/product
  • You are receiving ArchetypeCreatedIntegrationEvent payloads

References:

Klaviyo

  • A Klaviyo Private API Key
  • A pinned Klaviyo API revision header (date-based API versions)
  • Familiarity with Klaviyo profile properties + segmentation

References:


Step 0 — Choose the identifier strategy (don’t skip this)

Klaviyo supports profile identifiers like email (recommended), phone_number, and external_id.

If you can map X-External-Idemail 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:

  1. Read the raw request body (not JSON-parsed yet).
  2. Compute HMAC-SHA256 with SAHHA_WEBHOOK_SECRET.
  3. 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_id to 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

  1. In Klaviyo, search for the user profile (by email).
  2. Check Properties for keys like:
    • sahha_archetype_chronotype_weekly
    • sahha_archetype_activity_level_daily
  3. 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 someone
    • sahha_archetype_chronotype_weekly
    • equals
    • night_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:

  1. Go to Metrics
  2. Confirm you see a metric like:
    • Sahha Archetype Assigned
  3. Create a Flow triggered by that metric.
  4. Add a Flow Filter:
    • sahha_archetype_chronotype_weekly equals "early_bird"
  5. 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.id as a unique record key
  • createdAtUtc to 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

Klaviyo