Updated today Guides

Enrich Iterable User Profiles with Sahha Archetypes

A step-by-step tutorial to integrate Sahha archetype outputs into Iterable user profiles as custom 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 this guide does

You will connect Sahha Archetypes to Iterable so that every time Sahha assigns an archetype (weekly/monthly/quarterly), your system updates the corresponding Iterable user profile fields. Those fields can then be used for:

  • Segmentation (e.g., “short sleepers”)
  • Personalization (Handlebars merge tags)
  • Journey entry/branching (optionally via an event)

This guide intentionally focuses on profile enrichment (user fields), because that’s the most durable way to carry long-lived context into your CX system.


How Sahha Archetypes arrive

Sahha Archetypes are delivered as a JSON object per assignment, including name, value, ordinality, periodicity, and the time window (startDateTime, endDateTime) that the archetype represents. You can receive assignments as they’re created via Sahha Webhooks.


Architecture

flowchart LR
  A[Sahha] -->|Webhook: ArchetypeCreatedIntegrationEvent| B[Your Webhook Receiver]
  B -->|Update user fields| C[Iterable: /api/users/update]
  B -->|Optional: track event| D[Iterable: /api/events/track]

Why you need a receiver: Sahha sends webhooks to your endpoint. Your endpoint verifies the signature and then calls Iterable’s API.


Prerequisites

Sahha

  • You can create a webhook endpoint that is publicly reachable over HTTPS.
  • You have a Sahha webhook secret key (used to verify X-Signature).
  • Your Sahha profiles have a stable externalId that your backend can map to an Iterable user.

Iterable

  • A server-side Iterable API key.
  • Your Iterable base URL:
    • US: https://api.iterable.com
    • EU: https://api.eu.iterable.com

Step 1 — Decide your user identity mapping (do this first)

Sahha includes the user identifier in the webhook header X-External-Id. Your job is to map that identifier onto the correct user in Iterable.

Choose one of these approaches:

Set Sahha externalId = Iterable email

  • Your webhook receives X-External-Id
  • You call Iterable with email: externalId

Option B (if you prefer userId)

Maintain a mapping in your database:

  • Sahha externalId → Iterable userId (or email)
  • Your webhook receiver looks up the mapped identifier before calling Iterable

This tutorial implements Option A to keep the integration deterministic and easy to validate.


Step 2 — Define your Iterable field schema (avoid deep nesting)

Iterable supports nested objects, but mergeNestedObjects only merges one level deep; deeper nested objects will be overwritten when updated. To avoid accidental overwrites, this tutorial uses flat, namespaced user fields.

For each archetype assignment:

  • sahha_archetype_<name>_value (string)
  • sahha_archetype_<name>_ordinality (number, when applicable)
  • sahha_archetype_<name>_periodicity (string)
  • sahha_archetype_<name>_startDateTime (string / ISO)
  • sahha_archetype_<name>_endDateTime (string / ISO)
  • sahha_archetype_<name>_createdAtUtc (string / ISO)

Global helper fields (optional but useful):

  • sahha_archetypes_lastEventId (string)
  • sahha_archetypes_lastUpdatedAtUtc (string / ISO)

Example for sleep_duration:

  • sahha_archetype_sleep_duration_value = "short_sleeper"
  • sahha_archetype_sleep_duration_ordinality = 1
  • sahha_archetype_sleep_duration_periodicity = "monthly"

Schema hygiene tips

  • Keep names lowercase and stable. Create the fields in Iterable’s schema tools if your workspace requires pre-defined fields.
  • Only store what you’ll actually segment on. In many cases, *_value and *_ordinality are enough.

Step 3 — Create the Sahha webhook

Create a webhook in Sahha configured to send archetype assignments to your receiver endpoint:

  • Endpoint: https://YOUR_DOMAIN/webhooks/sahha
  • Event type: ArchetypeCreatedIntegrationEvent
  • Save the secret key used to verify X-Signature

What Sahha sends on every webhook request

Headers you must use:

  • X-Signature — HMAC-SHA256 hash of the raw payload using your secret key
  • X-External-Id — the profile’s external identifier (your join key)
  • X-Event-Type — event type string (route logic)

Step 4 — Implement the webhook receiver (Node.js + Express)

Key requirements

  1. Verify signature against the raw request body before doing anything else.
  2. Respond 200 quickly and process async.
  3. Handle duplicates (idempotency) using the event id.

Below is a production-oriented example that:

  • Verifies X-Signature
  • Parses the archetype payload
  • Flattens the update into user profile fields
  • Updates the Iterable user profile
  • Optionally tracks an Iterable event
import express from "express";
import crypto from "crypto";

const app = express();

/**
 * IMPORTANT:
 * Use a raw body parser for correct signature verification.
 * If your proxy/framework transforms the body, the signature check can fail.
 */
app.use("/webhooks/sahha", express.raw({ type: "application/json" }));

const SAHHA_WEBHOOK_SECRET = process.env.SAHHA_WEBHOOK_SECRET!;
const ITERABLE_API_KEY = process.env.ITERABLE_API_KEY!;
const ITERABLE_BASE_URL = process.env.ITERABLE_BASE_URL || "https://api.iterable.com"; // or https://api.eu.iterable.com

type SahhaArchetypeAssignment = {
  id: string;
  profileId?: string;
  accountId?: string;
  externalId: string;
  name: string;          // e.g. "sleep_duration"
  value: string;         // e.g. "short_sleeper"
  dataType?: string;     // "ordinal" or "categorical"
  ordinality?: number;   // numeric category ranking when applicable
  periodicity: string;   // e.g. "weekly" | "monthly" | "quarterly"
  startDateTime: string; // ISO 8601
  endDateTime: string;   // ISO 8601
  createdAtUtc: string;  // ISO 8601
};

function computeHmacSha256Hex(secret: string, rawBody: Buffer) {
  return crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
}

function safeFieldToken(input: string) {
  // Iterable field names are case and space sensitive.
  // Normalize archetype names into a safe token for field naming.
  return input
    .trim()
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "_")
    .replace(/^_+|_+$/g, "");
}

async function iterableUserUpdate(email: string, dataFields: Record<string, any>) {
  const resp = await fetch(`${ITERABLE_BASE_URL}/api/users/update`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Api-Key": ITERABLE_API_KEY,
    },
    body: JSON.stringify({
      email,
      dataFields,
      // Optional controls (depending on your Iterable project settings):
      // createNewFields: true,
      // mergeNestedObjects: false,
    }),
  });

  if (resp.status === 429) {
    // Implement backoff/retry in your queue worker.
    throw new Error("Iterable rate limited (429). Apply exponential backoff.");
  }

  if (!resp.ok) {
    const text = await resp.text();
    throw new Error(`Iterable users/update failed (${resp.status}): ${text}`);
  }
}

async function iterableTrackEvent(email: string, eventName: string, eventId: string, dataFields: Record<string, any>) {
  const resp = await fetch(`${ITERABLE_BASE_URL}/api/events/track`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Api-Key": ITERABLE_API_KEY,
    },
    body: JSON.stringify({
      email,
      eventName,
      id: eventId,        // useful for idempotency in Iterable event tracking
      dataFields,
    }),
  });

  if (!resp.ok) {
    const text = await resp.text();
    throw new Error(`Iterable events/track failed (${resp.status}): ${text}`);
  }
}

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" });
  }

  const rawBody = req.body as Buffer;
  const computed = computeHmacSha256Hex(SAHHA_WEBHOOK_SECRET, rawBody);

  if (signature.toLowerCase() !== computed.toLowerCase()) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // Acknowledge immediately. Do heavy work async.
  res.status(200).json({ received: true });

  try {
    if (eventType !== "ArchetypeCreatedIntegrationEvent") return;

    const payload: SahhaArchetypeAssignment = JSON.parse(rawBody.toString("utf8"));

    // Option A mapping: externalId == Iterable email
    const email = externalId;

    const archetypeNameToken = safeFieldToken(payload.name);

    // Flatten Sahha archetype assignment into Iterable user fields
    const dataFields: Record<string, any> = {
      [`sahha_archetype_${archetypeNameToken}_value`]: payload.value,
      [`sahha_archetype_${archetypeNameToken}_periodicity`]: payload.periodicity,
      [`sahha_archetype_${archetypeNameToken}_startDateTime`]: payload.startDateTime,
      [`sahha_archetype_${archetypeNameToken}_endDateTime`]: payload.endDateTime,
      [`sahha_archetype_${archetypeNameToken}_createdAtUtc`]: payload.createdAtUtc,

      // global helpers
      sahha_archetypes_lastEventId: payload.id,
      sahha_archetypes_lastUpdatedAtUtc: payload.createdAtUtc,
    };

    // Only set ordinality when it exists (ordinal archetypes)
    if (typeof payload.ordinality === "number") {
      dataFields[`sahha_archetype_${archetypeNameToken}_ordinality`] = payload.ordinality;
    }

    await iterableUserUpdate(email, dataFields);

    // Optional: track an event for journey triggers / reporting
    await iterableTrackEvent(
      email,
      "sahha_archetype_assigned",
      payload.id,
      {
        sahhaArchetypeName: payload.name,
        sahhaArchetypeValue: payload.value,
        sahhaArchetypePeriodicity: payload.periodicity,
        sahhaArchetypeOrdinality: payload.ordinality ?? null,
        sahhaArchetypeStartDateTime: payload.startDateTime,
        sahhaArchetypeEndDateTime: payload.endDateTime,
        sahhaArchetypeCreatedAtUtc: payload.createdAtUtc,
      }
    );
  } catch (err) {
    console.error("Failed processing Sahha webhook:", err);
  }
});

app.listen(3000, () => console.log("Listening on :3000"));

Step 5 — Test the integration end-to-end

1) Validate webhook delivery + signature verification

  • Trigger a test webhook from Sahha (or use a real profile with archetypes).
  • Confirm your server logs show:
    • header presence (X-Signature, X-External-Id, X-Event-Type)
    • successful signature verification
    • successful Iterable API responses

2) Validate the user field update in Iterable

In Iterable, find a user (the email matching externalId) and confirm fields exist:

  • sahha_archetype_<name>_value
  • sahha_archetypes_lastUpdatedAtUtc

3) Validate segmentation

Create a segment like:

  • sahha_archetype_sleep_duration_value equals short_sleeper

If the segment populates, your enrichment pipeline is working.

4) Validate personalization (Handlebars)

Use a message template and reference the field:

Your current sleep archetype is: {{sahha_archetype_sleep_duration_value}}

Step 6 — Use archetypes in CX campaigns (examples)

Example A: “Short sleepers” recovery messaging

Segment:

  • sahha_archetype_sleep_duration_value = short_sleeper

Then tailor:

  • content timing, tone, and call-to-action based on the archetype.

Example B: Branching a journey based on ordinality

If ordinality exists:

  • Lower ordinality = “less”
  • Higher ordinality = “more”

Branch rules might look like:

  • sahha_archetype_sleep_duration_ordinality <= 1 → “help users stabilize basics”
  • >= 2 → “help users optimize”

Example C: Event-triggered entry (optional)

If you track sahha_archetype_assigned, you can:

  • Trigger a journey when a new archetype is assigned
  • Add a filter on event payload properties (name/value)

Operational checklist (so it doesn’t break in production)

Idempotency

  • Use the Sahha event id to prevent double-processing.
  • A simple approach: store lastProcessedSahhaEventId in your DB per user and skip repeats.

Ordering

  • If you receive multiple assignments for the same user close together, process in a queue.
  • Prefer “latest by createdAtUtc” if events arrive out of order.

Rate limits + retries

  • Handle 429 responses from Iterable with exponential backoff.
  • Use a queue worker to retry safely (don’t retry inline inside the webhook request).

Schema hygiene

  • Keep fields namespaced (sahha_...) and consistent.
  • Avoid uncontrolled creation of new fields due to typos in archetype names (normalize field tokens as shown).

Data minimization

  • Archetypes are category labels; avoid sending raw biometric time-series into Iterable unless you have a clear use case and consent basis.

Quick reference: field mapping

Sahha FieldIterable User Field
nameused to form sahha_archetype_<name>_...
valuesahha_archetype_<name>_value
ordinalitysahha_archetype_<name>_ordinality
periodicitysahha_archetype_<name>_periodicity
startDateTimesahha_archetype_<name>_startDateTime
endDateTimesahha_archetype_<name>_endDateTime
createdAtUtcsahha_archetype_<name>_createdAtUtc
idsahha_archetypes_lastEventId (optional)

Common failure modes and fixes

“Invalid signature”

  • Ensure you computed HMAC over the raw request body.
  • Ensure you used the correct secret key and hex digest.

“User not found in Iterable”

  • Confirm externalId is the user’s email (Option A), or implement your mapping lookup (Option B).

“Fields appear but segments don’t work”

  • Confirm field names match exactly (case-sensitive).
  • Confirm values are strings vs numbers where expected.

“Updates overwrite other data”

  • Use flat fields as shown (no deep object nesting).
  • Avoid sending a nested object unless you fully control all keys within it.

Next enhancements (optional)

  • Batch updates with /api/users/bulkUpdate if you process large volumes.
  • Keep a compact “current archetypes summary” field (stringified JSON) for debugging:
    • sahha_archetypes_debug_json (but don’t use it for segmentation).
  • Add a nightly reconciliation job if you need to guarantee Iterable matches Sahha (for example, re-fetch and re-sync).