Updated today Guides

Enrich Braze User Profiles with Sahha Archetypes

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

Overview

This guide shows how to take Sahha Archetypes (longer-term behavioral classifications) and attach them to Braze user profiles as custom attributes, so you can:

  • build segments using behavioral “personas”
  • personalize message copy using Liquid
  • optionally trigger Canvas flows when an archetype changes

Sahha Archetypes are categorical labels (for example, chronotype = early_bird) that update on a defined periodicity (weekly/monthly/quarterly), and can include an ordinality rank where relevant. Archetypes are also available via webhooks as they’re assigned.


What you’ll build

A simple pipeline:

  1. Sahha → Webhook: your server receives an ArchetypeCreatedIntegrationEvent
  2. Transform: map Sahha’s archetype payload to a stable Braze attribute naming convention
  3. Braze → /users/track: write those values to the matching Braze user via external_id
  4. Campaigns: use the attributes in Segments, Canvases, and Liquid

Prerequisites

Sahha

  • Sahha account with Webhooks enabled
  • Access to configure a webhook and retrieve your webhook secret
  • Archetypes enabled (or being generated)

Braze

  • Braze workspace access
  • Your Braze cluster REST endpoint (example: https://rest.iad-01.braze.com)
  • A Braze REST API key with permission to call Track Users (/users/track)

Your backend

  • A public HTTPS endpoint to receive webhooks (server, serverless function, or edge worker)
  • Ability to store secrets (environment variables)

The most important design choice: user identity

To enrich a Braze profile, you must have a single user identifier shared across systems.

Recommendation: use externalId everywhere

  • Sahha webhook events include externalId (and also provide X-External-Id in headers).
  • Braze user updates commonly use external_id (Braze’s external user identifier).

Rule: The value you send to Sahha as externalId must be the exact same value you use as external_id in Braze.

Client-side: ensure Braze knows the external user ID

If you use Braze SDKs, set the user ID after login/registration (e.g., changeUser("user_123")).
Do this before you expect campaigns/segments to behave consistently.


Step 1 — Collect Braze REST endpoint + create a REST API key

1. Find your cluster REST endpoint

Braze workspaces run on “clusters”. Your REST base URL must match your cluster.

Examples look like:

  • https://rest.iad-01.braze.com
  • https://rest.us-10.braze.com

You can find the cluster mapping in Braze documentation and/or your dashboard instance settings.

2. Create a REST API key

Create an API key that can call:

  • Track Users (/users/track)

Optional (nice for QA):

  • Export Users by IDs (/users/export/ids)

Store:

  • BRAZE_REST_ENDPOINT
  • BRAZE_REST_API_KEY

Step 2 — Configure a Sahha Archetypes webhook

In the Sahha Dashboard:

  1. Go to Webhooks
  2. Add your HTTPS endpoint URL
  3. Select the event type: ArchetypeCreatedIntegrationEvent
  4. Copy your secret key (you’ll need it to verify signatures)
  5. Use Send Test Event to confirm your endpoint receives requests

Delivery expectations (important for backend design)

  • Sahha retries failed deliveries (and events may arrive out of order)
  • Your endpoint should respond quickly and process asynchronously when possible
  • Use the webhook event id for idempotency

Step 3 — Decide on a Braze attribute naming scheme

Braze custom attributes are key-value fields on the user profile. You want a naming scheme that is:

  • stable over time
  • easy to segment on
  • easy to personalize in Liquid
  • compatible with multiple archetypes + periodicities

Suggested convention

For each archetype event, write:

  1. Value attribute

    • Key: sahha_archetype_<periodicity>__<name>
    • Value: <value> (string)
  2. Ordinality attribute (when present)

    • Key: sahha_archetype_<periodicity>__<name>_ordinality
    • Value: <ordinality> (integer)
  3. Last updated timestamp

    • Key: sahha_archetype_last_updated_at_utc
    • Value: <createdAtUtc> (ISO string)

Examples:

  • sahha_archetype_weekly__chronotype = "early_bird"
  • sahha_archetype_weekly__chronotype_ordinality = 0
  • sahha_archetype_monthly__sleep_duration = "short_sleeper"
  • sahha_archetype_monthly__sleep_duration_ordinality = 1

Why include periodicity in the key?
Because the same archetype name can exist at different periodicities, and you may want to segment differently.


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

Below is a complete example you can deploy as a small service.

It will:

  • verify X-Signature using HMAC-SHA256 over the raw request body
  • read X-Event-Type and only process ArchetypeCreatedIntegrationEvent
  • map to Braze custom attributes
  • POST to POST /users/track to update the user

Note: The code uses express.text({ type: '*/*' }) so you can hash the raw body string correctly.

Environment variables

# Required
SAHHA_WEBHOOK_SECRET="..."
BRAZE_REST_ENDPOINT="https://rest.iad-01.braze.com"
BRAZE_REST_API_KEY="..."

# Optional
BRAZE_SEND_ARCHETYPE_EVENT="true"   # also send a Braze custom event for Canvas triggers

Server code

/**
 * Sahha → Braze Archetypes bridge
 *
 * Receives Sahha ArchetypeCreatedIntegrationEvent
 * Writes to Braze user profile as custom attributes via POST /users/track
 */

const express = require("express");
const crypto = require("crypto");

const app = express();
app.use(express.text({ type: "*/*" })); // IMPORTANT: raw body as string

const SAHHA_WEBHOOK_SECRET = process.env.SAHHA_WEBHOOK_SECRET;
const BRAZE_REST_ENDPOINT = process.env.BRAZE_REST_ENDPOINT;
const BRAZE_REST_API_KEY = process.env.BRAZE_REST_API_KEY;
const BRAZE_SEND_ARCHETYPE_EVENT = (process.env.BRAZE_SEND_ARCHETYPE_EVENT || "false") === "true";

if (!SAHHA_WEBHOOK_SECRET || !BRAZE_REST_ENDPOINT || !BRAZE_REST_API_KEY) {
  throw new Error("Missing required env vars: SAHHA_WEBHOOK_SECRET, BRAZE_REST_ENDPOINT, BRAZE_REST_API_KEY");
}

function timingSafeEqual(a, b) {
  // Compare strings in constant time
  const bufA = Buffer.from(a, "utf8");
  const bufB = Buffer.from(b, "utf8");
  if (bufA.length !== bufB.length) return false;
  return crypto.timingSafeEqual(bufA, bufB);
}

function verifySahhaSignature(rawBody, signatureHeader) {
  const hmac = crypto.createHmac("sha256", SAHHA_WEBHOOK_SECRET);
  const computed = hmac.update(rawBody).digest("hex");
  return timingSafeEqual(computed.toLowerCase(), (signatureHeader || "").toLowerCase());
}

function toSafeKey(str) {
  // Braze custom attribute keys are best kept simple.
  // Convert any unexpected characters to underscores.
  return String(str).toLowerCase().replace(/[^a-z0-9_]+/g, "_");
}

function buildBrazeAttributesFromArchetype(archetype) {
  const periodicity = toSafeKey(archetype.periodicity); // e.g. weekly
  const name = toSafeKey(archetype.name);               // e.g. chronotype
  const keyBase = `sahha_archetype_${periodicity}__${name}`;

  const attrs = {
    [keyBase]: archetype.value,
    sahha_archetype_last_updated_at_utc: archetype.createdAtUtc,
  };

  if (typeof archetype.ordinality === "number") {
    attrs[`${keyBase}_ordinality`] = archetype.ordinality;
  }

  return attrs;
}

async function postToBrazeUsersTrack({ externalId, attributes, archetypeEvent }) {
  const url = `${BRAZE_REST_ENDPOINT.replace(/\/$/, "")}/users/track`;

  const body = {
    attributes: [
      {
        external_id: externalId,
        ...attributes,
      },
    ],
  };

  // Optional: send a Braze custom event so you can trigger Canvases on archetype assignment.
  // Note: webhooks can retry — if you enable this, implement idempotency (see below).
  if (BRAZE_SEND_ARCHETYPE_EVENT) {
    body.events = [
      {
        external_id: externalId,
        name: "sahha_archetype_assigned",
        time: archetypeEvent.createdAtUtc,
        properties: {
          sahha_archetype_id: archetypeEvent.id,
          name: archetypeEvent.name,
          value: archetypeEvent.value,
          periodicity: archetypeEvent.periodicity,
          ordinality: archetypeEvent.ordinality,
          startDateTime: archetypeEvent.startDateTime,
          endDateTime: archetypeEvent.endDateTime,
          version: archetypeEvent.version,
        },
      },
    ];
  }

  const resp = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${BRAZE_REST_API_KEY}`,
    },
    body: JSON.stringify(body),
  });

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

  return resp.json();
}

async function processEventAsync(eventType, externalIdFromHeader, payload) {
  if (eventType !== "ArchetypeCreatedIntegrationEvent") return;

  // Prefer externalId from payload, fallback to header
  const externalId = payload.externalId || externalIdFromHeader;
  if (!externalId) throw new Error("Missing externalId");

  // --- Optional idempotency ---
  // Sahha recommends using event.id to handle duplicates on retry.
  // If you enable BRAZE_SEND_ARCHETYPE_EVENT, duplicates may trigger multiple Canvas entries.
  // Typical approach: store processed payload.id in a DB with TTL and skip if already processed.
  //
  // if (await alreadyProcessed(payload.id)) return;
  // await markProcessed(payload.id);

  const attributes = buildBrazeAttributesFromArchetype(payload);

  const result = await postToBrazeUsersTrack({
    externalId,
    attributes,
    archetypeEvent: payload,
  });

  return result;
}

app.post("/webhooks/sahha", (req, res) => {
  const signature = req.get("X-Signature");
  const externalId = req.get("X-External-Id");
  const eventType = req.get("X-Event-Type");

  if (!signature || !eventType) {
    return res.status(400).json({ error: "Missing required headers: X-Signature and/or X-Event-Type" });
  }

  const rawBody = req.body || "";
  if (!verifySahhaSignature(rawBody, signature)) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // Respond quickly; do heavier work async
  res.status(200).json({ received: true });

  const payload = JSON.parse(rawBody);
  processEventAsync(eventType, externalId, payload).catch((err) => {
    // Replace with your logger/observability tooling
    console.error("Sahha webhook processing failed:", err);
  });
});

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

Step 5 — Test end-to-end

A. Test your Sahha webhook receiver

Use Send Test Event in the Sahha dashboard and confirm your service logs show:

  • eventType = ArchetypeCreatedIntegrationEvent
  • signature verification success
  • a successful Braze response

B. Test Braze directly with curl

This helps isolate “Braze-side” issues (key/endpoint/auth).

curl --request POST \
  --url "$BRAZE_REST_ENDPOINT/users/track" \
  --header 'Content-Type: application/json' \
  --header "Authorization: Bearer $BRAZE_REST_API_KEY" \
  --data '{
    "attributes": [
      {
        "external_id": "user_12345",
        "sahha_archetype_weekly__chronotype": "early_bird",
        "sahha_archetype_weekly__chronotype_ordinality": 0,
        "sahha_archetype_last_updated_at_utc": "2025-01-22T06:00:00Z"
      }
    ]
  }'

Expected: a JSON response with "message": "success" and an attributes_processed count.

C. Verify in Braze Dashboard

In Braze:

  1. Find the user by external_id
  2. Confirm the custom attributes appear on the profile

Step 6 — Use Sahha Archetypes in Braze campaigns

A. Segmentation examples

Create segments based on custom attributes, for example:

  • Chronotype segment

    • sahha_archetype_weekly__chronotype equals night_owl
  • Sleep duration segment

    • sahha_archetype_monthly__sleep_duration equals short_sleeper
  • Ordinality-based segment

    • sahha_archetype_monthly__sleep_duration_ordinality less than 2

B. Canvas entry (optional)

If you enabled BRAZE_SEND_ARCHETYPE_EVENT=true, you can trigger a Canvas from the event:

  • custom event: sahha_archetype_assigned
  • filter on event properties such as name or value

Important: implement idempotency using Sahha’s webhook id so retries don’t cause duplicate Canvas entries.

C. Message personalization (Liquid)

Use Braze Liquid to inject the user’s archetype into copy:

Your current chronotype is: {{custom_attribute.${sahha_archetype_weekly__chronotype}}}

You can also write conditional logic:

{% if {{custom_attribute.${sahha_archetype_weekly__chronotype}}} == "night_owl" %}
Try scheduling your next workout later in the day.
{% else %}
Morning sessions might be your sweet spot.
{% endif %}

Operational considerations

1. Payload limits and batching

If you ever batch multiple updates:

  • keep requests under Braze payload limits
  • respect /users/track per-request object caps (attributes/events/purchases)

2. Event ordering

Sahha events may arrive out of order. Use createdAtUtc for sequencing if you maintain a “latest known” record.

3. Time-to-respond

Sahha expects a quick 2xx. Do heavy work async (queue/job) if you anticipate spikes.


Common pitfalls

“Braze updated the wrong user”

Almost always an identity mismatch:

  • Sahha externalId differs from Braze external_id
  • or Braze user hasn’t been identified (SDK changeUser) before messaging begins

“Signature verification fails”

Common causes:

  • parsing JSON before hashing (must hash raw body)
  • whitespace/encoding differences
  • using the wrong secret key

“My segment filter doesn’t show results”

  • no users have received the attribute yet
  • attribute key mismatch (typos or different naming scheme)
  • wrong workspace/cluster

Optional: Backfill archetypes for existing users

If you already have users in Braze and want to populate archetypes without waiting for new webhook events:

  1. Use Sahha’s API to query archetype assignments for each profile (see Sahha docs: Archetypes → “API Query archetype assignments for any profile”).
  2. Batch updates to Braze via /users/track (up to the per-request object caps).
  3. Rate limit your job and add retries + dead-letter handling.

A simple backfill job is typically:

  • read user list (from your DB or Braze export)
  • call Sahha archetype query
  • write latest values into Braze as custom attributes

(Links included for convenience)

Sahha Webhooks: https://docs.sahha.ai/docs/connect/webhooks
Sahha Webhook Events (ArchetypeCreatedIntegrationEvent): https://docs.sahha.ai/docs/connect/webhooks/events
Sahha Archetypes overview: https://docs.sahha.ai/docs/products/archetypes

Braze API endpoint clusters: https://www.braze.com/docs/user_guide/administrative/access_braze/sdk_endpoints
Braze User Data endpoints: https://www.braze.com/docs/api/endpoints/user_data
Braze /users/track endpoint: https://www.braze.com/docs/api/endpoints/user_data/post_user_track
Braze Liquid tags (custom attributes): https://www.braze.com/docs/user_guide/personalization_and_dynamic_content/liquid/supported_personalization_tags