Overview
Sahha Archetypes categorize longer-term behavioral patterns into stable labels you can use for segmentation and personalization (for example: chronotype: early_bird, activity_level: highly_active). Because archetypes refresh on longer cycles (daily/weekly/monthly depending on the archetype), they’re ideal for audience building and lifecycle automation rather than minute-by-minute nudges.
In this guide you’ll build a webhook receiver that:
- Receives
ArchetypeCreatedIntegrationEventwebhooks from Sahha - Verifies the webhook signature (recommended)
- Sends the data into Segment using:
- Identify to set archetype traits on the user
- Track to log each archetype assignment/update as an event
Architecture
flowchart LR
A[Sahha Webhooks] -->|ArchetypeCreatedIntegrationEvent| B[Your Webhook Receiver]
B -->|Identify + Track| C[Segment Source]
C --> D[Destinations: Braze / Customer.io / Warehouse / etc.]
What you’ll need
- A Segment workspace with permission to create Sources
- A Segment Write Key from a Node.js Source (recommended)
- A Sahha project with Webhooks enabled
- A publicly reachable HTTPS endpoint for your webhook receiver
- Your Sahha Webhook Secret Key (for signature verification)
- Node.js 18+
Step 1 — Create a Segment Source and get your Write Key
- In Segment, go to Connections → Sources
- Click Add Source
- Select Node.js
- Open the Source → Settings
- Copy the Write Key
Store it as an environment variable:
export SEGMENT_WRITE_KEY="YOUR_WRITE_KEY"
Step 2 — Enable Sahha Webhooks for Archetypes
In the Sahha Dashboard:
- Go to Webhooks
- Add your endpoint URL (must be HTTPS)
- Select event type:
ArchetypeCreatedIntegrationEvent - Copy your Webhook Secret Key
Store it as an environment variable:
export SAHHA_WEBHOOK_SECRET="YOUR_SAHHA_WEBHOOK_SECRET"
Sahha includes these headers on every webhook request:
X-Signature— HMAC-SHA256 hash of the raw payload using your secret keyX-External-Id— your external identifier for the profileX-Event-Type— event type string (for exampleArchetypeCreatedIntegrationEvent)
Step 3 — Choose your mapping strategy (recommended defaults)
Identify (traits): archetypes as user traits
Set stable traits on the Segment user profile:
sahha_archetype_chronotype = "early_bird"sahha_archetype_activity_level = "highly_active"
This is the easiest way to create Segment audiences and sync traits into downstream tools.
Track (events): archetype assignment as an event
Record a Track event like:
- Event name:
Sahha Archetype Assigned - Properties:
archetype_name,archetype_value,periodicity,startDateTime,endDateTime, timestamps, etc.
This helps with analytics, debugging, and historical analysis.
Step 4 — Build the webhook receiver (Node + Express)
Signature verification requires the raw request body, so we parse requests as text (not JSON) to compute the HMAC correctly.
4.1 Create a project and install dependencies
mkdir sahha-segment-webhook
cd sahha-segment-webhook
npm init -y
npm i express dotenv @segment/analytics-node
4.2 Create server.js
const express = require('express')
const crypto = require('crypto')
require('dotenv').config()
const { Analytics } = require('@segment/analytics-node')
const app = express()
// IMPORTANT: Keep the raw body for HMAC verification.
// Do NOT use express.json() unless you also capture raw bytes.
app.use(express.text({ type: '*/*' }))
const SAHHA_WEBHOOK_SECRET = process.env.SAHHA_WEBHOOK_SECRET
const SEGMENT_WRITE_KEY = process.env.SEGMENT_WRITE_KEY
const SEGMENT_HOST = process.env.SEGMENT_HOST // Optional (EU workspaces). Example: https://eu1.api.segmentapis.com
if (!SAHHA_WEBHOOK_SECRET) throw new Error('Missing SAHHA_WEBHOOK_SECRET')
if (!SEGMENT_WRITE_KEY) throw new Error('Missing SEGMENT_WRITE_KEY')
const analytics = new Analytics({
writeKey: SEGMENT_WRITE_KEY,
...(SEGMENT_HOST ? { host: SEGMENT_HOST } : {}),
})
// Log delivery errors (recommended)
analytics.on('error', (err) => {
console.error('Segment error:', err)
})
function safeEqual(a, b) {
const aBuf = Buffer.from(String(a ?? ''), 'utf8')
const bBuf = Buffer.from(String(b ?? ''), 'utf8')
if (aBuf.length !== bBuf.length) return false
return crypto.timingSafeEqual(aBuf, bBuf)
}
function verifySahhaSignature(rawBody, signatureHeader) {
const computed = crypto
.createHmac('sha256', SAHHA_WEBHOOK_SECRET)
.update(rawBody)
.digest('hex')
return safeEqual(String(signatureHeader ?? ''), computed)
}
function toTraitKey(archetypeName) {
// Keep keys stable + destination-friendly.
// e.g. "sleep_pattern" -> "sahha_archetype_sleep_pattern"
const normalized = String(archetypeName ?? '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '')
return `sahha_archetype_${normalized}`
}
app.post('/webhooks/sahha', (req, res) => {
const signature = req.get('X-Signature')
const externalIdHeader = req.get('X-External-Id')
const eventType = req.get('X-Event-Type')
const rawBody = req.body
// Validate required headers early
if (!signature || !externalIdHeader || !eventType) {
return res.status(400).json({ error: 'Missing required Sahha webhook headers' })
}
// Verify signature
if (!verifySahhaSignature(rawBody, signature)) {
return res.status(401).json({ error: 'Invalid signature' })
}
// Acknowledge quickly (Sahha retries on non-2xx)
res.status(200).json({ received: true })
// Only handle archetype events here
if (eventType !== 'ArchetypeCreatedIntegrationEvent') return
let payload
try {
payload = JSON.parse(rawBody)
} catch (e) {
console.error('Invalid JSON body:', e)
return
}
// Sahha Archetype payload fields (common)
const {
id,
profileId,
accountId,
externalId: externalIdBody,
name,
value,
dataType,
periodicity,
ordinality,
startDateTime,
endDateTime,
createdAtUtc,
version,
} = payload || {}
// Optional sanity check: header external id should match payload external id (if present)
if (externalIdBody && String(externalIdBody) !== String(externalIdHeader)) {
console.error('External ID mismatch between header and payload', {
header: externalIdHeader,
body: externalIdBody,
})
return
}
if (!name || value === undefined || value === null) {
console.error('Missing archetype name/value in payload')
return
}
const userId = String(externalIdHeader)
// 1) Identify: write archetype trait onto the user
analytics.identify({
userId,
traits: {
[toTraitKey(name)]: value,
// Optional metadata traits (keep or remove based on your needs)
sahha_profile_id: profileId,
sahha_account_id: accountId,
sahha_archetype_last_updated_utc: createdAtUtc,
},
})
// Optional: Group by account if you model B2B tenants in Segment
// analytics.group({
// userId,
// groupId: String(accountId),
// traits: { sahha_account_id: accountId },
// })
// 2) Track: emit an event for analytics and downstream triggers
analytics.track({
userId,
event: 'Sahha Archetype Assigned',
properties: {
sahha_event_id: id,
sahha_profile_id: profileId,
sahha_account_id: accountId,
archetype_name: name,
archetype_value: value,
archetype_data_type: dataType,
archetype_periodicity: periodicity,
archetype_ordinality: ordinality,
period_start: startDateTime,
period_end: endDateTime,
created_at_utc: createdAtUtc,
algorithm_version: version,
},
})
})
app.get('/health', (_, res) => res.status(200).send('ok'))
// Graceful shutdown: flush queued events before exit
async function shutdown(signal) {
console.log(`Received ${signal}. Flushing Segment queue...`)
try {
await analytics.flush({ close: true })
} catch (e) {
console.error('Error flushing Segment queue:', e)
} finally {
process.exit(0)
}
}
process.on('SIGINT', () => shutdown('SIGINT'))
process.on('SIGTERM', () => shutdown('SIGTERM'))
const port = process.env.PORT || 3000
app.listen(port, () => console.log(`Listening on :${port}`))
4.3 Run locally
export SAHHA_WEBHOOK_SECRET="..."
export SEGMENT_WRITE_KEY="..."
node server.js
Deploy this service (Render, Fly.io, Cloud Run, etc.), then set your Sahha webhook URL to:
https://YOUR_DOMAIN/webhooks/sahha
Step 5 — Add idempotency (strongly recommended)
Webhook systems retry. To prevent duplicate Segment calls, treat payload.id as an idempotency key.
Recommended approach:
- Store processed
payload.idin Redis (or your DB) - Use a TTL of 7–30 days
- If the key exists, skip Segment calls
Pseudo-code:
if (await redis.get(`sahha:event:${id}`)) return
await redis.set(`sahha:event:${id}`, '1', { EX: 60 * 60 * 24 * 30 })
Step 6 — Validate in Segment
- Open Segment → your Node.js Source → Debugger
- Trigger a test event from the Sahha Dashboard (Send Test Event)
- Confirm you see:
- An
Identifycall with traits likesahha_archetype_chronotype - A
Trackevent namedSahha Archetype Assigned
- An
Segment EU workspaces (regional ingest)
If your Segment workspace is configured for EU regional ingest, set a host for the Node SDK.
Set:
export SEGMENT_HOST="https://eu1.api.segmentapis.com"
Then restart your service.
If you’re not sure whether you need this, check your Segment workspace’s regional settings. Symptoms of a mismatch often look like: requests succeed, but events never appear in the Segment debugger.
Optional — Only forward changes (reduce downstream noise)
If you only want to trigger journeys when an archetype value changes:
- Store last known
{userId, archetype_name, periodicity} -> value - If the incoming
valueequals the stored value, skiptrack(or send a lower-priority event)
This keeps destinations like Braze / Customer.io cleaner.
Optional — Backfill current archetype state into Segment
If you want Segment traits to reflect a user’s current archetype state (especially for users who existed before you set up webhooks):
- For each user
externalId, fetch archetype assignments from Sahha’s API - Send one
identifycall per user containing the current archetype traits - Optionally emit historical
trackevents if your destinations accept historical timestamps
Backfill pattern (pseudo-code):
for (const archetype of sahhaArchetypes) {
analytics.identify({
userId: externalId,
traits: { [`sahha_archetype_${archetype.name}`]: archetype.value },
})
}
Recommended naming conventions
- Track event name:
Sahha Archetype Assigned - Identify trait keys:
sahha_archetype_${name} - Track properties:
archetype_name,archetype_value,archetype_periodicity,period_start,period_end
Keep names stable to avoid breaking downstream audiences and automations.
Security and privacy notes
- Verify webhook signatures and require HTTPS.
- Only forward what you need; archetypes are already high-level labels.
- Treat archetypes as sensitive user context; ensure your consent and privacy policy cover analytics/engagement use.
- Avoid medical claims or diagnoses based on archetypes; use them for personalization and segmentation only.
Next steps
- Build Segment audiences from archetype traits (for example “Early Birds”, “Night Owls”, “Highly Active”)
- Sync audiences to engagement tools and trigger lifecycle journeys
- Add Score events (Sleep/Activity/Wellbeing/Readiness) if you want more dynamic, state-based automation