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:
- Sahha → Webhook: your server receives an
ArchetypeCreatedIntegrationEvent - Transform: map Sahha’s archetype payload to a stable Braze attribute naming convention
- Braze → /users/track: write those values to the matching Braze user via
external_id - 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 provideX-External-Idin 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.comhttps://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_ENDPOINTBRAZE_REST_API_KEY
Step 2 — Configure a Sahha Archetypes webhook
In the Sahha Dashboard:
- Go to Webhooks
- Add your HTTPS endpoint URL
- Select the event type:
ArchetypeCreatedIntegrationEvent - Copy your secret key (you’ll need it to verify signatures)
- 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
idfor 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:
-
Value attribute
- Key:
sahha_archetype_<periodicity>__<name> - Value:
<value>(string)
- Key:
-
Ordinality attribute (when present)
- Key:
sahha_archetype_<periodicity>__<name>_ordinality - Value:
<ordinality>(integer)
- Key:
-
Last updated timestamp
- Key:
sahha_archetype_last_updated_at_utc - Value:
<createdAtUtc>(ISO string)
- Key:
Examples:
sahha_archetype_weekly__chronotype = "early_bird"sahha_archetype_weekly__chronotype_ordinality = 0sahha_archetype_monthly__sleep_duration = "short_sleeper"sahha_archetype_monthly__sleep_duration_ordinality = 1
Why include periodicity in the key?
Because the same archetypenamecan 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-Signatureusing HMAC-SHA256 over the raw request body - read
X-Event-Typeand only processArchetypeCreatedIntegrationEvent - map to Braze custom attributes
- POST to
POST /users/trackto 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:
- Find the user by
external_id - 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__chronotypeequalsnight_owl
-
Sleep duration segment
sahha_archetype_monthly__sleep_durationequalsshort_sleeper
-
Ordinality-based segment
sahha_archetype_monthly__sleep_duration_ordinalityless than2
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
nameorvalue
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/trackper-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
externalIddiffers from Brazeexternal_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:
- Use Sahha’s API to query archetype assignments for each profile (see Sahha docs: Archetypes → “API Query archetype assignments for any profile”).
- Batch updates to Braze via
/users/track(up to the per-request object caps). - 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
Reference links
(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