Updated today Guides

Enrich Customer.io User Profiles with Sahha Archetypes

A step-by-step tutorial to integrate Sahha archetype outputs into Customer.io 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.

Customer.io: Enrich user profiles with Sahha Archetypes

This guide shows how to synchronize Sahha Archetypes (weekly/monthly behavioral archetypes) into Customer.io user attributes, so you can build segments and campaigns based on behavior patterns.

You’ll implement:

  1. Real-time sync via Sahha Webhooks (ArchetypeCreatedIntegrationEvent)
  2. Optional backfill via the Sahha Archetypes API
  3. Optional event tracking in Customer.io to trigger journeys when archetypes change

What you’ll build

When Sahha assigns an archetype (e.g., sleep_duration = short_sleeper), your integration will:

  • Verify the webhook signature (security)
  • Map the archetype record into stable Customer.io attributes
  • Update the corresponding Customer.io person via the Track API

Example resulting Customer.io attributes:

  • sahha_archetype_monthly_sleep_duration = "short_sleeper"
  • sahha_archetype_monthly_sleep_duration_ordinality = 1
  • sahha_archetype_monthly_sleep_duration_window_start_ts = 1735657200
  • sahha_archetype_monthly_sleep_duration_window_end_ts = 1738252800
  • sahha_archetype_last_updated_ts = 1738415333

Prerequisites

Sahha

  • Sahha account
  • A configured webhook endpoint (HTTPS)
  • Your Sahha webhook secret key (for X-Signature verification)

Customer.io

  • Customer.io Site ID and Track API Key (a “secret key”)
  • You know whether your workspace identifies people by id or email

Important: The cleanest mapping is to use the same identifier for:

  • Sahha externalId
  • Customer.io {identifier} (path parameter)

This guide assumes externalId is the Customer.io identifier.


Step 1 — Determine your Customer.io Track API base URL (region-safe)

Customer.io accounts can live in different regions. Use the region lookup endpoint to get the correct base URL for subsequent Track API requests.

curl --request GET \
  --user 'YOUR_SITE_ID:YOUR_API_SECRET_KEY' \
  --url https://track.customer.io/api/v1/accounts/region

The response returns a url like https://track.customer.io (US) or another regional URL. Use that returned URL as your CIO_BASE_URL.

Set environment variables:

export CIO_SITE_ID="YOUR_SITE_ID"
export CIO_API_KEY="YOUR_API_SECRET_KEY"
export CIO_BASE_URL="https://track.customer.io" # replace with the returned 'url'

Step 2 — Decide how you’ll identify users (avoid “Attribute Update Failure”)

Customer.io’s “Add or update a customer” endpoint updates a person based on {identifier} in the URL. If you send conflicting identifiers in the request body, you can get “Attribute Update Failure” behavior (even if the API returns 200 OK).

Recommendation (safest):

  • Put your identifier only in the URL path
  • Do not include id or email in the body unless you explicitly need to set/update identifiers

We’ll update the user like this:

  • PUT /api/v1/customers/{externalId}
  • Body contains only attributes (+ _timestamp, optionally _update)

Step 3 — Configure Sahha Webhooks to send Archetypes

In the Sahha Dashboard:

  1. Go to Webhooks
  2. Add your endpoint URL (must be HTTPS)
  3. Select the event type: ArchetypeCreatedIntegrationEvent
  4. Copy your secret key (you’ll use it to verify X-Signature)

Sahha sends these headers with every webhook:

  • X-Signature (HMAC-SHA256 of the raw payload)
  • X-External-Id (the user identifier in your system)
  • X-Event-Type (e.g., ArchetypeCreatedIntegrationEvent)

Also note:

  • Sahha retries failed deliveries (up to 5 times with backoff)
  • Events may arrive out of order — use timestamps for sequencing

Step 4 — Map Sahha Archetypes into Customer.io attributes

Sahha archetype fields you’ll receive

An archetype assignment includes:

  • name (e.g., sleep_duration)
  • value (e.g., short_sleeper)
  • periodicity (e.g., weekly, monthly)
  • ordinality (integer)
  • startDateTime, endDateTime (analysis window)
  • createdAtUtc (when the archetype was created)
  • id (UUID of the archetype record)

Attribute naming strategy (flat + predictable)

Customer.io “Attributes” are commonly used as primitives. Keep it flat:

  • sahha_archetype_${periodicity}_${name}
  • sahha_archetype_${periodicity}_${name}_ordinality
  • sahha_archetype_${periodicity}_${name}_window_start_ts
  • sahha_archetype_${periodicity}_${name}_window_end_ts
  • sahha_archetype_${periodicity}_${name}_created_ts
  • sahha_archetype_last_updated_ts

This yields stable segments like:

  • “monthly_sleep_duration is short_sleeper”
  • “weekly_activity_level ordinality >= 2”

Step 5 — Implement the webhook receiver (Node.js / Express)

This example:

  • Uses express.text() so you can hash the raw body
  • Verifies X-Signature
  • Deduplicates by Sahha payload.id (recommended because Sahha retries)
  • Converts createdAtUtc to a Unix timestamp
  • Updates Customer.io user attributes via Track API

Install

npm i express node-fetch

Server code

import express from "express";
import crypto from "crypto";
import fetch from "node-fetch";

const app = express();

// IMPORTANT: we need the raw request body for signature verification
app.use(express.text({ type: "*/*" }));

const SAHHA_WEBHOOK_SECRET = process.env.SAHHA_WEBHOOK_SECRET;

const CIO_SITE_ID = process.env.CIO_SITE_ID;
const CIO_API_KEY = process.env.CIO_API_KEY;
const CIO_BASE_URL = process.env.CIO_BASE_URL;

// A minimal in-memory dedupe store for tutorial purposes.
// In production, store this in Redis/Dynamo/Postgres with TTL.
const processed = new Set();

function unixSeconds(isoString) {
  // createdAtUtc may include fractional seconds; Date handles it.
  return Math.floor(new Date(isoString).getTime() / 1000);
}

function basicAuthHeader(user, pass) {
  const token = Buffer.from(`${user}:${pass}`, "utf8").toString("base64");
  return `Basic ${token}`;
}

async function updateCustomerIoAttributes(identifier, attrs, eventTs, { updateOnly = false } = {}) {
  const url = `${CIO_BASE_URL}/api/v1/customers/${encodeURIComponent(identifier)}`;

  const body = {
    ...attrs,

    // Customer.io uses this to order attribute updates if multiple arrive quickly
    _timestamp: eventTs,

    // If you ONLY want to update existing profiles and never create new ones:
    // set _update:true to prevent accidental new profile creation.
    ...(updateOnly ? { _update: true } : {}),
  };

  const res = await fetch(url, {
    method: "PUT",
    headers: {
      Authorization: basicAuthHeader(CIO_SITE_ID, CIO_API_KEY),
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`Customer.io update failed (${res.status}): ${text}`);
  }
}

async function trackCustomerIoEvent(identifier, name, data, { timestamp } = {}) {
  const url = `${CIO_BASE_URL}/api/v1/customers/${encodeURIComponent(identifier)}/events`;

  const body = {
    name,
    data,
    ...(timestamp ? { timestamp } : {}),
  };

  const res = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: basicAuthHeader(CIO_SITE_ID, CIO_API_KEY),
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (!res.ok) {
    const text = await res.text().catch(() => "");
    throw new Error(`Customer.io event failed (${res.status}): ${text}`);
  }
}

app.post("/webhooks/sahha", async (req, res) => {
  try {
    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 Sahha signature: HMAC-SHA256 of raw body using the Sahha webhook secret
    const computed = crypto
      .createHmac("sha256", SAHHA_WEBHOOK_SECRET)
      .update(req.body)
      .digest("hex");

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

    // Acknowledge receipt quickly (recommended), then do work async.
    // For tutorial clarity, we'll do work inline but still respond fast.
    const payload = JSON.parse(req.body);

    // Only handle archetypes for this guide
    if (eventType !== "ArchetypeCreatedIntegrationEvent") {
      return res.status(200).json({ received: true, ignored: true });
    }

    // Deduplicate webhook retries using Sahha archetype record id
    if (payload?.id && processed.has(payload.id)) {
      return res.status(200).json({ received: true, duplicate: true });
    }
    if (payload?.id) processed.add(payload.id);

    // Sahha Archetype payload fields
    const periodicity = payload.periodicity;       // weekly/monthly
    const archetypeName = payload.name;            // e.g., sleep_duration
    const archetypeValue = payload.value;          // e.g., short_sleeper
    const ordinality = payload.ordinality ?? null; // integer
    const startDateTime = payload.startDateTime;
    const endDateTime = payload.endDateTime;
    const createdAtUtc = payload.createdAtUtc;

    // Use Sahha createdAtUtc to order updates (helps when events arrive out of order)
    const eventTs = unixSeconds(createdAtUtc);

    // Flatten attributes for Customer.io
    const keyBase = `sahha_archetype_${periodicity}_${archetypeName}`;
    const attrs = {
      [keyBase]: archetypeValue,
      [`${keyBase}_ordinality`]: ordinality ?? 0,
      [`${keyBase}_window_start_ts`]: startDateTime ? unixSeconds(startDateTime) : eventTs,
      [`${keyBase}_window_end_ts`]: endDateTime ? unixSeconds(endDateTime) : eventTs,
      [`${keyBase}_created_ts`]: eventTs,
      sahha_archetype_last_updated_ts: eventTs,
    };

    // Update Customer.io profile (externalId becomes the Track API identifier)
    await updateCustomerIoAttributes(externalId, attrs, eventTs, {
      updateOnly: false, // set true if you want to avoid creating profiles
    });

    // Optional: track an event for journey triggers
    // (If you do this, you can trigger a journey on "sahha_archetype_assigned".)
    await trackCustomerIoEvent(
      externalId,
      "sahha_archetype_assigned",
      {
        periodicity,
        name: archetypeName,
        value: archetypeValue,
        ordinality: ordinality ?? 0,
      },
      { timestamp: eventTs }
    );

    return res.status(200).json({ received: true });
  } catch (err) {
    // Returning non-2xx triggers Sahha retries; only do this for transient errors.
    return res.status(500).json({ error: err.message || "Unhandled error" });
  }
});

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

Step 6 — Test end-to-end

1) Send a test webhook from Sahha

Use the Send Test Event button in the Sahha Dashboard for your webhook.

2) Validate in Customer.io

In Customer.io, check the user profile and confirm:

  • The sahha_archetype_* attributes exist
  • The values match the webhook payload
  • If you enabled the optional event, confirm the user event appears

Step 7 — Build segments and journeys in Customer.io

Once attributes are available, you can build:

  • Segments that filter users by archetype attributes
    Example: sahha_archetype_monthly_sleep_duration = short_sleeper

  • Journeys/Campaigns triggered by the optional event
    Example trigger: event name sahha_archetype_assigned
    Then branch using conditions on data.periodicity, data.name, data.value.


Optional: Backfill archetypes for existing users (Sahha API)

If you want users to have archetypes in Customer.io immediately (instead of waiting for the next weekly/monthly recomputation), you can backfill via Sahha’s Archetypes API.

1) Get an Account Token

curl --request POST \
  --url https://api.sahha.ai/api/v1/oauth/account/token \
  --header 'Content-Type: application/json' \
  --data '{
    "clientId": "YOUR_CLIENT_ID",
    "clientSecret": "YOUR_CLIENT_SECRET"
  }'

Response includes an accountToken.

2) Query archetypes for a profile (by externalId)

curl --request GET \
  --url "https://api.sahha.ai/api/v1/profile/archetypes/YOUR_EXTERNAL_ID" \
  --header "Authorization: account YOUR_ACCOUNT_TOKEN"

This returns archetype assignments with fields like: name, value, ordinality, periodicity, startDateTime, endDateTime, createdAtUtc.

3) Write a backfill script

High-level steps:

  1. Iterate your user list (the same identifiers you use in Customer.io)
  2. For each user:
    • Call Sahha archetypes endpoint
    • For each returned archetype record:
      • Apply the same attribute mapping as the webhook handler
      • Update the user in Customer.io with PUT /customers/{identifier}

Tip: For backfill, set Customer.io _timestamp to the archetype record’s createdAtUtc (converted to unix seconds). This keeps ordering stable if you later receive late webhooks.


Reliability + correctness checklist

Security

  • Verify X-Signature using the raw body and your Sahha secret key
  • Reject requests missing required headers

Idempotency

  • Deduplicate webhook retries using Sahha payload id

Ordering

  • Use Sahha createdAtUtc as Customer.io _timestamp so newer archetypes win if deliveries arrive out of order

Avoid profile duplication

  • If you are updating users that must already exist in Customer.io, set _update: true
  • If you want Customer.io to create the user if missing, leave _update unset/false

Avoid identifier conflicts

  • Put the identifier in the request path
  • Avoid sending id/email in the body unless you explicitly need identifier management

Troubleshooting

“200 OK but I don’t see attributes updating”

Common causes:

  • You updated the wrong region URL (run the region lookup again)
  • Your {identifier} doesn’t match the actual user identifier in Customer.io
  • You sent conflicting identifiers in the request body, causing “Attribute Update Failure” patterns

Sahha retries keep happening

  • Ensure you return 2xx quickly
  • If Customer.io is down transiently, return 500 so Sahha retries
  • Otherwise, handle errors internally and still return 200 to avoid repeated replays

Summary

You now have a repeatable pattern to:

  • Receive Sahha archetypes in real-time
  • Store them as stable Customer.io attributes
  • Segment and message users based on behavioral archetypes
  • Optionally trigger journeys via a dedicated event

If you want to extend this pattern, the same approach applies to Sahha Scores and Biomarkers (attributes + optional trigger events).