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 mParticle via the Events API using:
- user attributes for “current archetype state”
- a custom event (
Sahha Archetype Assigned) for analytics, journeys, and debugging
Architecture
flowchart LR
A[Sahha Webhooks] -->|ArchetypeCreatedIntegrationEvent| B[Your Webhook Receiver]
B -->|POST /v2/events| C[mParticle Events API]
C --> D[mParticle Audiences / Journeys / Destinations]
What you’ll need
- An mParticle workspace with permission to create an Input (Custom Feed)
- Your mParticle Server-to-Server API Key and API Secret
- A Sahha project with Webhooks enabled
- A publicly reachable HTTPS webhook endpoint
- Your Sahha Webhook Secret Key (for signature verification)
- Node.js 18+
Step 1 — Create an mParticle Input (Custom Feed) and get credentials
In mParticle, create a Custom Feed Input to generate server-to-server credentials:
- You’ll receive:
- API Key (username)
- API Secret (password)
Store them as environment variables:
export MPARTICLE_API_KEY="YOUR_MPARTICLE_API_KEY"
export MPARTICLE_API_SECRET="YOUR_MPARTICLE_API_SECRET"
Step 2 — Choose the correct mParticle Events API base URL (pod)
mParticle’s Events API base URL depends on your data localization pod. Use the one that matches your mParticle org:
- US1:
https://s2s.mparticle.com/v2(orhttps://s2s.us1.mparticle.com/v2) - US2:
https://s2s.us2.mparticle.com/v2 - EU1:
https://s2s.eu1.mparticle.com/v2 - AU1:
https://s2s.au1.mparticle.com/v2
Set:
export MPARTICLE_BASE_URL="https://s2s.mparticle.com/v2"
Step 3 — 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:
export SAHHA_WEBHOOK_SECRET="YOUR_SAHHA_WEBHOOK_SECRET"
Sahha includes these headers on webhook requests:
X-Signature— HMAC-SHA256 hex digest of the raw payload using your secretX-External-Id— your external identifier for the profileX-Event-Type— event type string (for exampleArchetypeCreatedIntegrationEvent)
Step 4 — Decide your mapping strategy in mParticle
mParticle’s server-to-server model is “one request = one batch for one user”.
We’ll send:
A) User attributes (recommended): archetypes as current state
We set stable user attributes like:
sahha_archetype_chronotype = "early_bird"sahha_archetype_activity_level = "highly_active"
This makes it easy to build audiences and drive downstream personalization.
B) Custom event (recommended): log each assignment/update
We send a custom event:
event_type:custom_eventdata.event_name:Sahha Archetype Assigneddata.custom_event_type:otherdata.custom_attributes: includes the archetype fields and Sahha metadata
This supports journeys, analytics, QA, and debugging.
Step 5 — 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.
5.1 Create a project and install dependencies
mkdir sahha-mparticle-webhook
cd sahha-mparticle-webhook
npm init -y
npm i express dotenv
5.2 Create server.js
const express = require('express')
const crypto = require('crypto')
require('dotenv').config()
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 MPARTICLE_API_KEY = process.env.MPARTICLE_API_KEY
const MPARTICLE_API_SECRET = process.env.MPARTICLE_API_SECRET
const MPARTICLE_BASE_URL = process.env.MPARTICLE_BASE_URL || 'https://s2s.mparticle.com/v2'
const MPARTICLE_ENVIRONMENT = process.env.MPARTICLE_ENVIRONMENT || 'production' // development|production
if (!SAHHA_WEBHOOK_SECRET) throw new Error('Missing SAHHA_WEBHOOK_SECRET')
if (!MPARTICLE_API_KEY) throw new Error('Missing MPARTICLE_API_KEY')
if (!MPARTICLE_API_SECRET) throw new Error('Missing MPARTICLE_API_SECRET')
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')
// Compare case-insensitively
return safeEqual(String(signatureHeader ?? '').toLowerCase(), computed.toLowerCase())
}
function toUserAttributeKey(archetypeName) {
// Example: "activity_level" => "sahha_archetype_activity_level"
const normalized = String(archetypeName ?? '')
.trim()
.toLowerCase()
.replace(/[^a-z0-9_]/g, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '')
return `sahha_archetype_${normalized}`
}
function basicAuthHeader(key, secret) {
const token = Buffer.from(`${key}:${secret}`, 'utf8').toString('base64')
return `Basic ${token}`
}
async function sendToMparticle({ userId, sahhaPayload, rawBody }) {
const {
id,
profileId,
accountId,
externalId,
name,
value,
dataType,
periodicity,
ordinality,
startDateTime,
endDateTime,
createdAtUtc,
version,
} = sahhaPayload || {}
// Use Sahha's record id as mParticle's source_request_id for de-duplication
// (mParticle deduplicates by source_request_id scoped to API key for ~30-60 days).
const sourceRequestId = String(id || crypto.createHash('sha256').update(rawBody).digest('hex'))
const nowMs = Date.now()
const eventTimestampMs = createdAtUtc ? Date.parse(createdAtUtc) : nowMs
const mpBatch = {
// Strongly recommended for idempotency
source_request_id: sourceRequestId,
// mParticle schema
schema_version: 2,
environment: MPARTICLE_ENVIRONMENT,
// Identity: map Sahha external ID into an mParticle identity field
// Most common: customer_id
user_identities: {
customer_id: String(userId),
},
// "Current state" on the user
user_attributes: {
[toUserAttributeKey(name)]: value,
// Optional metadata traits (keep or remove based on your privacy posture)
sahha_profile_id: profileId,
sahha_account_id: accountId,
sahha_archetype_last_updated_utc: createdAtUtc,
},
// Log a custom event for analytics/journeys/debugging
events: [
{
event_type: 'custom_event',
data: {
event_name: 'Sahha Archetype Assigned',
custom_event_type: 'other',
timestamp_unixtime_ms: Number.isFinite(eventTimestampMs) ? eventTimestampMs : nowMs,
custom_attributes: {
sahha_event_id: id,
sahha_profile_id: profileId,
sahha_account_id: accountId,
sahha_external_id: externalId,
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,
},
},
},
],
}
const url = `${MPARTICLE_BASE_URL}/events`
const resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: basicAuthHeader(MPARTICLE_API_KEY, MPARTICLE_API_SECRET),
},
body: JSON.stringify(mpBatch),
})
if (!resp.ok) {
const text = await resp.text().catch(() => '')
throw new Error(`mParticle Events API error: ${resp.status} ${resp.statusText} ${text}`)
}
}
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
if (!signature || !externalIdHeader || !eventType) {
return res.status(400).json({ error: 'Missing required Sahha webhook headers' })
}
if (!verifySahhaSignature(rawBody, signature)) {
return res.status(401).json({ error: 'Invalid signature' })
}
// Acknowledge quickly (webhook providers retry on non-2xx / timeouts)
res.status(200).json({ received: true })
if (eventType !== 'ArchetypeCreatedIntegrationEvent') return
let payload
try {
payload = JSON.parse(rawBody)
} catch (e) {
console.error('Invalid JSON body:', e)
return
}
// Optional: sanity check between header and payload externalId (if present)
if (payload && payload.externalId && String(payload.externalId) !== String(externalIdHeader)) {
console.error('External ID mismatch between header and payload', {
header: externalIdHeader,
body: payload.externalId,
})
return
}
// Fire-and-forget; log errors.
// In production, you may prefer a queue (SQS/PubSub/Redis) for stronger delivery guarantees.
sendToMparticle({ userId: externalIdHeader, sahhaPayload: payload, rawBody })
.catch((err) => console.error('Forwarding to mParticle failed:', err))
})
app.get('/health', (_, res) => res.status(200).send('ok'))
const port = process.env.PORT || 3000
app.listen(port, () => console.log(`Listening on :${port}`))
5.3 Run locally
export SAHHA_WEBHOOK_SECRET="..."
export MPARTICLE_API_KEY="..."
export MPARTICLE_API_SECRET="..."
export MPARTICLE_BASE_URL="https://s2s.mparticle.com/v2"
export MPARTICLE_ENVIRONMENT="development"
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 6 — Validate in mParticle
Use mParticle debugging tools:
- Trigger a test webhook from the Sahha Dashboard (Send Test Event)
- In mParticle, open:
- Live Stream (typically shows development/device specific data)
- your Input’s event viewer (if applicable)
You should see:
- A custom event named
Sahha Archetype Assigned - User attributes such as
sahha_archetype_activity_level
Idempotency (important)
You should expect retries and occasional duplicates.
A) mParticle-side de-duplication via source_request_id
mParticle supports a batch-level source_request_id used to deduplicate inbound requests (retained for ~30–60 days, scoped by API key). In this guide we set:
source_request_id = Sahha payload.id
This gives you solid, low-effort idempotency.
B) Optional: your own de-duplication store
If you want extra protection, store processed Sahha ids in Redis/DB:
if (await redis.get(`sahha:event:${payload.id}`)) return
await redis.set(`sahha:event:${payload.id}`, '1', { EX: 60 * 60 * 24 * 30 })
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 incoming
valueequals stored value, skip sending the custom event - Still update the user attribute if you want “last updated” timestamps refreshed
This keeps audiences/journeys and downstream tools cleaner.
Optional — Batch multiple users with /bulkevents
If you process a high volume of events, mParticle supports:
POST /v2/bulkeventsto send up to 100 user batches per request
This is useful if you buffer events in a queue and flush in batches.
Troubleshooting
Signature verification fails
- Ensure you hash the raw request body exactly as received (do not re-stringify JSON).
- Ensure you’re using the correct Sahha webhook secret.
- Ensure you’re comparing hex digests case-insensitively.
mParticle accepts requests but you don’t see events
- Confirm
MPARTICLE_BASE_URLmatches your pod (US1/US2/EU1/AU1). - Confirm
MPARTICLE_ENVIRONMENTmatches the view you’re using (development vs production). - Check response logs for non-2xx statuses.
Wrong identity mapping
- This guide uses
user_identities.customer_id = externalId. - If your mParticle workspace expects a different identity type (email, other, etc.), adjust
user_identitiesaccordingly.
Recommended naming conventions
- User attribute keys:
sahha_archetype_${name} - Custom event name:
Sahha Archetype Assigned - Custom attributes:
archetype_name,archetype_value,archetype_periodicity,period_start,period_end
Keep names stable to avoid breaking 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 + privacy policy cover analytics/engagement uses.
- Avoid medical claims or diagnoses based on archetypes; use them for personalization and segmentation only.
References
- Sahha Webhooks:
https://docs.sahha.ai/docs/connect/webhooks - Sahha Webhook Events (includes
ArchetypeCreatedIntegrationEventschema):https://docs.sahha.ai/docs/connect/webhooks/events - mParticle Events API (base URLs + auth):
https://docs.mparticle.com/developers/apis/http/ - mParticle JSON Reference (batch structure,
source_request_id, events):https://docs.mparticle.com/developers/apis/json-reference/ - mParticle HTTP Quickstart (create input + example payload):
https://docs.mparticle.com/developers/quickstart/http/create-input/