title: “Send Sahha Archetypes to Amplitude User Properties” description: “Ingest Sahha Archetype webhooks and write them to Amplitude user properties for cohorts, personalization, and downstream activation.”
What this guide does
You will connect Sahha Archetypes to Amplitude so that when Sahha assigns an archetype (e.g. weekly/monthly), your integration:
- Receives a Sahha webhook (
ArchetypeCreatedIntegrationEvent) - Verifies the webhook signature (
X-Signature) - Maps the archetype into Amplitude user properties
- Sends an Identify API call to set those user properties in Amplitude
- (Optional) Sends a lightweight event via HTTP V2 API so the updated user properties apply immediately in Amplitude
Outcome: your Sahha archetypes become first-class segmentation fields in Amplitude cohorts and can be activated to downstream destinations from Amplitude.
Architecture
flowchart LR
A[Sahha] -->|Webhook: ArchetypeCreatedIntegrationEvent| B[Your Webhook Receiver]
B -->|Identify API: set user properties| C[Amplitude]
B -->|Optional: HTTP V2 event| C
Prerequisites
Sahha
- Webhooks enabled and configured
- A webhook secret key (used for
X-Signatureverification) - Archetypes enabled for your account/product
- Stable
externalIdvalues on profiles (your join key)
Amplitude
- An Amplitude Project API Key
- Your region endpoint (Standard or EU residency)
Step 1 — Decide the identity strategy (critical)
Amplitude Identify calls require either:
user_id, ordevice_id
For this integration, you should use user_id.
Recommended mapping
- Sahha
externalId→ Amplitudeuser_id
If your Sahha externalId is not the same as the Amplitude user_id, add a lookup step (e.g., database mapping) in your webhook receiver.
Important: In Amplitude, if you change a user’s user_id from an existing value, Amplitude can create a new user. Treat user_id as immutable once established. (See Identify API considerations.)
Step 2 — Choose a safe user property schema
Amplitude user properties should be:
- stable (don’t rename keys)
- flat (easy cohort filters)
- namespaced (avoid collisions)
Recommended naming convention
For each archetype assignment from Sahha:
sahha_archetype_<name>_<periodicity>=<value>(string)sahha_archetype_<name>_<periodicity>_ordinality=<ordinality>(number, if present)sahha_archetype_<name>_<periodicity>_start=<startDateTime>(ISO string)sahha_archetype_<name>_<periodicity>_end=<endDateTime>(ISO string)sahha_archetype_<name>_<periodicity>_created_at_utc=<createdAtUtc>(ISO string)
Global helpers (optional):
sahha_archetypes_last_event_id=<id>sahha_archetypes_last_updated_at_utc=<createdAtUtc>
Example:
sahha_archetype_chronotype_weekly = "night_owl"sahha_archetype_chronotype_weekly_ordinality = 2
Step 3 — Configure the Sahha webhook
In Sahha, configure a webhook with:
- Endpoint:
https://YOUR_DOMAIN/webhooks/sahha - Event type:
ArchetypeCreatedIntegrationEvent - Copy the webhook secret key
Sahha sends these headers on webhook requests:
X-SignatureX-External-IdX-Event-Type
Step 4 — Implement the receiver and update Amplitude via Identify API
Amplitude’s Identify API:
- Endpoint:
- Standard:
https://api2.amplitude.com/identify - EU:
https://api.eu.amplitude.com/identify
- Standard:
- Authentication:
api-key=API_KEYin the body - Request format:
application/x-www-form-urlencodedcontaining:api_keyidentification(a JSON string of an object or array of objects)
Identify supports user property operations like $set, $setOnce, $add, etc. For this integration, we use $set only.
Node.js (Express) implementation
This example:
- Verifies Sahha
X-Signatureagainst the raw payload - Maps Sahha archetype fields into Amplitude user properties
- Calls Amplitude Identify with
$set - Optionally fires a lightweight “sync” event (HTTP V2) to ensure the updated user properties apply immediately
import express from "express";
import crypto from "crypto";
const app = express();
// IMPORTANT: raw body is required for signature verification
app.use("/webhooks/sahha", express.raw({ type: "application/json" }));
const SAHHA_WEBHOOK_SECRET = process.env.SAHHA_WEBHOOK_SECRET!;
const AMPLITUDE_API_KEY = process.env.AMPLITUDE_API_KEY!;
const AMPLITUDE_REGION = (process.env.AMPLITUDE_REGION || "standard").toLowerCase(); // "standard" | "eu"
const AMPLITUDE_SEND_SYNC_EVENT = (process.env.AMPLITUDE_SEND_SYNC_EVENT || "true").toLowerCase() === "true";
if (!SAHHA_WEBHOOK_SECRET) throw new Error("Missing SAHHA_WEBHOOK_SECRET");
if (!AMPLITUDE_API_KEY) throw new Error("Missing AMPLITUDE_API_KEY");
type SahhaArchetypeCreatedIntegrationEvent = {
id: string;
profileId: string;
accountId: string;
externalId: string;
name: string;
value: string;
dataType: string;
periodicity: string;
ordinality?: number;
startDateTime: string;
endDateTime: string;
createdAtUtc: string;
version: number;
};
function amplitudeIdentifyEndpoint() {
return AMPLITUDE_REGION === "eu"
? "https://api.eu.amplitude.com/identify"
: "https://api2.amplitude.com/identify";
}
function amplitudeHttpV2Endpoint() {
return AMPLITUDE_REGION === "eu"
? "https://api.eu.amplitude.com/2/httpapi"
: "https://api2.amplitude.com/2/httpapi";
}
function computeHmacSha256Hex(secret: string, rawBody: Buffer) {
return crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
}
function timingSafeHexEquals(aHex: string, bHex: string) {
const a = Buffer.from(aHex, "hex");
const b = Buffer.from(bHex, "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
function safeToken(input: string) {
return input
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_+|_+$/g, "");
}
function mapArchetypeToAmplitudeUserProps(a: SahhaArchetypeCreatedIntegrationEvent): Record<string, string | number> {
const name = safeToken(a.name);
const periodicity = safeToken(a.periodicity);
const base = `sahha_archetype_${name}_${periodicity}`;
const props: Record<string, string | number> = {
[base]: a.value,
[`${base}_start`]: a.startDateTime,
[`${base}_end`]: a.endDateTime,
[`${base}_created_at_utc`]: a.createdAtUtc,
sahha_archetypes_last_event_id: a.id,
sahha_archetypes_last_updated_at_utc: a.createdAtUtc,
};
if (typeof a.ordinality === "number") {
props[`${base}_ordinality`] = a.ordinality;
}
return props;
}
async function amplitudeIdentify(userId: string, userPropsToSet: Record<string, string | number>) {
// Identify API expects x-www-form-urlencoded with api_key and identification JSON
const identification = [
{
user_id: userId,
user_properties: {
$set: userPropsToSet,
},
},
];
const body = new URLSearchParams();
body.set("api_key", AMPLITUDE_API_KEY);
body.set("identification", JSON.stringify(identification));
const res = await fetch(amplitudeIdentifyEndpoint(), {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (res.status === 429) {
throw new Error("Amplitude Identify throttled (429). Backoff ~15s and retry per docs.");
}
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Amplitude Identify failed (${res.status}): ${text}`);
}
}
async function amplitudeSendSyncEvent(userId: string, insertId: string) {
// HTTP V2 API: send a small event to ensure updated user properties apply immediately.
// time MUST be ms since epoch.
const nowMs = Date.now();
const payload = {
"api-key": AMPLITUDE_API_KEY,
events: [
{
user_id: userId,
event_type: "sahha_archetype_sync",
time: nowMs,
insert_id: insertId,
},
],
};
const res = await fetch(amplitudeHttpV2Endpoint(), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (res.status === 429) {
throw new Error("Amplitude HTTP V2 throttled (429). Backoff and retry.");
}
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(`Amplitude HTTP V2 failed (${res.status}): ${text}`);
}
}
app.post("/webhooks/sahha", async (req, res) => {
const signature = req.get("X-Signature") || "";
const externalIdHeader = req.get("X-External-Id") || "";
const eventType = req.get("X-Event-Type") || "";
if (!signature || !externalIdHeader || !eventType) {
return res.status(400).json({ error: "Missing required headers" });
}
const rawBody = req.body as Buffer;
const computed = computeHmacSha256Hex(SAHHA_WEBHOOK_SECRET, rawBody);
if (!timingSafeHexEquals(signature.toLowerCase(), computed.toLowerCase())) {
return res.status(401).json({ error: "Invalid signature" });
}
// ACK fast; do downstream work async
res.status(200).json({ received: true });
try {
if (eventType !== "ArchetypeCreatedIntegrationEvent") return;
const payload = JSON.parse(rawBody.toString("utf8")) as SahhaArchetypeCreatedIntegrationEvent;
// Identity strategy: externalId == Amplitude user_id
// If you need a mapping (externalId -> user_id), do it here.
const amplitudeUserId = externalIdHeader;
const userProps = mapArchetypeToAmplitudeUserProps(payload);
await amplitudeIdentify(amplitudeUserId, userProps);
if (AMPLITUDE_SEND_SYNC_EVENT) {
// Using Sahha event id as insert_id makes the sync event easier to dedupe/debug.
await amplitudeSendSyncEvent(amplitudeUserId, payload.id);
}
} catch (err) {
console.error("Failed processing Sahha webhook:", err);
// In production: enqueue for retry / DLQ
}
});
app.listen(3000, () => console.log("Listening on :3000"));
Step 5 — Validate in Amplitude
1) Confirm user properties exist
- Open User Lookup (or equivalent user profile view)
- Search for the user by the
user_idyou mapped from SahhaexternalId - Confirm you see keys like:
sahha_archetype_chronotype_weeklysahha_archetype_sleep_duration_monthly
2) Build a cohort from an archetype
Create a cohort rule like:
User Propertysahha_archetype_chronotype_weeklyequalsnight_owl
3) (Optional) Activate cohorts downstream
If you use Amplitude Audiences/Activation, you can sync cohorts to downstream tools (Braze, Customer.io, etc.) and use Sahha-powered cohorts as the source of truth.
Operational considerations
Rate limiting and throttling
Amplitude may drop user property updates if a single user’s properties are updated too frequently (for example, excessive syncing). Design your integration so you update archetypes at the cadence they’re produced (weekly/monthly), not on every app action.
Also handle HTTP 429 responses with backoff (and retries via a queue worker).
Out-of-order events and idempotency
Sahha may retry webhooks and events may arrive out of order. Use:
payload.idfor idempotency (store processed IDs)payload.createdAtUtcto decide “latest known” per archetype key if needed
“Why did my user property not show up yet?”
If you only send an Identify call, Amplitude notes that property values may not appear until the user’s next event. This is why the optional sahha_archetype_sync event can be useful.
Common pitfalls
Identity mismatch
If Sahha externalId doesn’t match Amplitude user_id, you’ll update the wrong record (or create a new record). Fix: add a deterministic mapping.
Nested object overwrites
Avoid nested user properties for archetypes; store flat strings/numbers so cohorts are predictable.
Signature verification fails
Make sure you verify HMAC against the raw request body exactly as received, before JSON parsing.
Reference links
Sahha
- https://docs.sahha.ai/docs/connect/webhooks
- https://docs.sahha.ai/docs/connect/webhooks/events
- https://docs.sahha.ai/docs/products/archetypes
- https://sahha.ai/archetypes-api