Updated today Guides

Send Sahha Archetype events to mParticle

Receive Sahha ArchetypeCreatedIntegrationEvent webhooks, verify signatures, and forward them to mParticle as user attributes + custom events.

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:

  1. Receives ArchetypeCreatedIntegrationEvent webhooks from Sahha
  2. Verifies the webhook signature (recommended)
  3. 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 (or https://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:

  1. Go to Webhooks
  2. Add your endpoint URL (must be HTTPS)
  3. Select event type: ArchetypeCreatedIntegrationEvent
  4. 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 secret
  • X-External-Id — your external identifier for the profile
  • X-Event-Type — event type string (for example ArchetypeCreatedIntegrationEvent)

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:

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.

We send a custom event:

  • event_type: custom_event
  • data.event_name: Sahha Archetype Assigned
  • data.custom_event_type: other
  • data.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:

  1. Trigger a test webhook from the Sahha Dashboard (Send Test Event)
  2. 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:

  1. Store last known {userId, archetype_name, periodicity} -> value
  2. If incoming value equals stored value, skip sending the custom event
  3. 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/bulkevents to 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_URL matches your pod (US1/US2/EU1/AU1).
  • Confirm MPARTICLE_ENVIRONMENT matches 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_identities accordingly.

  • 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 ArchetypeCreatedIntegrationEvent schema): 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/