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
externalIdthat 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
- US:
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:
Option A (recommended for simplicity)
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→ IterableuserId(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.
Recommended field naming convention
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 = 1sahha_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,
*_valueand*_ordinalityare 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 keyX-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
- Verify signature against the raw request body before doing anything else.
- Respond
200quickly and process async. - 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
- header presence (
2) Validate the user field update in Iterable
In Iterable, find a user (the email matching externalId) and confirm fields exist:
sahha_archetype_<name>_valuesahha_archetypes_lastUpdatedAtUtc
3) Validate segmentation
Create a segment like:
sahha_archetype_sleep_duration_valueequalsshort_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
idto prevent double-processing. - A simple approach: store
lastProcessedSahhaEventIdin 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
429responses 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 Field | Iterable User Field |
|---|---|
name | used to form sahha_archetype_<name>_... |
value | sahha_archetype_<name>_value |
ordinality | sahha_archetype_<name>_ordinality |
periodicity | sahha_archetype_<name>_periodicity |
startDateTime | sahha_archetype_<name>_startDateTime |
endDateTime | sahha_archetype_<name>_endDateTime |
createdAtUtc | sahha_archetype_<name>_createdAtUtc |
id | sahha_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
externalIdis 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/bulkUpdateif 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).