Updated today Guides

Send Sahha Archetype events to Segment

Receive Sahha ArchetypeCreatedIntegrationEvent webhooks, verify signatures, and forward them to Segment as Identify (traits) + Track (events) for audiences, journeys, and downstream tools.

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 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

  1. In Segment, go to Connections → Sources
  2. Click Add Source
  3. Select Node.js
  4. Open the Source → Settings
  5. 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:

  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 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 key
  • X-External-Id — your external identifier for the profile
  • X-Event-Type — event type string (for example ArchetypeCreatedIntegrationEvent)

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


Webhook systems retry. To prevent duplicate Segment calls, treat payload.id as an idempotency key.

Recommended approach:

  • Store processed payload.id in 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

  1. Open Segment → your Node.js Source → Debugger
  2. Trigger a test event from the Sahha Dashboard (Send Test Event)
  3. Confirm you see:
    • An Identify call with traits like sahha_archetype_chronotype
    • A Track event named Sahha Archetype Assigned

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:

  1. Store last known {userId, archetype_name, periodicity} -> value
  2. If the incoming value equals the stored value, skip track (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):

  1. For each user externalId, fetch archetype assignments from Sahha’s API
  2. Send one identify call per user containing the current archetype traits
  3. Optionally emit historical track events 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 },
  })
}

  • 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