Updated today Guides

Enrich OneSignal User Profiles with Sahha Archetypes

Integrate Sahha archetype outputs into OneSignal user tags for smarter segmentation and personalized messaging based on health and lifestyle patterns.

Send Sahha Archetypes to OneSignal User Tags

This guide shows how to enrich OneSignal user profiles with Sahha Archetypes so you can segment and personalize CX campaigns using OneSignal Data Tags.

You will:

  1. Configure Sahha to emit ArchetypeCreatedIntegrationEvent webhooks.
  2. Receive and verify webhook requests in your backend.
  3. Update OneSignal user tags via the OneSignal Update user API.
  4. Use tags in OneSignal Segments / Journeys.

Why this pattern exists

  • Sahha computes long-term behavioral archetypes (e.g., chronotype, sleep pattern) over time windows (weekly/monthly, etc.).
  • OneSignal tags are stored on the User, and are the most direct mechanism for segmentation and personalization.

Your backend bridges the two systems so you can keep credentials secure and enforce business rules (whitelisting, tag budgets, dedupe).


What you’re building (high level)

Sahha Webhook (ArchetypeCreatedIntegrationEvent) Your backend (verify signature + map identity + transform) OneSignal Update User (PATCH /apps/APP_ID/users/by/external_id/EXTERNAL_ID) Tags available for Segments / Journeys / Message personalization

Prerequisites

1) Use the same user identifier in both systems

This integration depends on a consistent identity:

  • In Sahha webhooks, the user identity is externalId (also sent via the X-External-Id header).
  • In OneSignal, the most common alias is external_id.

Recommended approach
Use your internal user ID as:

  • Sahha externalId
  • OneSignal external_id

If these aren’t aligned, you will enrich the wrong OneSignal user (or no user at all).


2) Ensure OneSignal users are identified (external_id exists)

Users often start “anonymous” in OneSignal until you assign an external ID.

In your client app, identify the user early (example shown conceptually; follow the SDK docs for your platform):

// Example concept: identify user so OneSignal associates subscriptions to external_id
OneSignal.login("user_12345");

If you don’t identify users, OneSignal may not have a user record addressable by external_id.


3) Configure the Sahha webhook

In the Sahha Dashboard:

  • Add your webhook endpoint URL (HTTPS required)
  • Select event type: ArchetypeCreatedIntegrationEvent
  • Copy the webhook secret key (used for signature verification)
  • Use Send Test Event to validate your endpoint

Sahha sends these headers (used in verification + routing):

  • X-Signature
  • X-External-Id
  • X-Event-Type

4) OneSignal credentials (server-side only)

You need:

  • OneSignal app_id
  • OneSignal App API Key (used with Authorization: Key YOUR_APP_API_KEY)

Never ship OneSignal API keys to client apps.


OneSignal tag rules you must follow

Tag values must be strings

Even numbers/timestamps/booleans must be stringified.

Examples:

  • "42" not 42
  • "1685400000" for unix timestamps (seconds)
  • "true" / "false" or "1" / "0"

Avoid restricted tag keys

OneSignal reserves certain keywords for internal message personalization.
Avoid common internal words like message, user, template, etc. (see OneSignal docs).

Recommendation: prefix every key with sahha_ or sahha_archetype_.

Plan limits: tags per user

OneSignal limits how many tags can exist on a user at one time depending on plan. If a user is at the limit, you may need to delete tags first before adding new ones.

Recommendation: Keep your Sahha tag footprint small and intentional.


Suggested tag mapping strategy

The Sahha archetype webhook payload includes at least:

  • name (category)
  • value (assigned label/value)
  • periodicity (daily/weekly/monthly)
  • startDateTime / endDateTime (analysis window)
  • createdAtUtc (assignment time)

A practical OneSignal mapping:

Sahha FieldOneSignal Tag KeyOneSignal Tag Value
name + valuesahha_archetype_${name}${value}
periodicitysahha_archetype_${name}_periodicity${periodicity}
endDateTimesahha_archetype_${name}_window_endunix seconds string
createdAtUtcsahha_archetypes_updated_atunix seconds string

Keep it minimal: In many cases, you only need sahha_archetype_${name}.


Implementation: webhook receiver → OneSignal update

Below is a production-friendly Node/Express implementation that:

  • reads the raw body (required for signature verification)
  • verifies Sahha HMAC signature
  • only processes the archetype event
  • updates the OneSignal user by external_id
  • stringifies all tag values
  • enforces an allowlist (to protect your OneSignal tag budget)

Environment variables

# Sahha
SAHHA_WEBHOOK_SECRET="your_sahha_webhook_secret"

# OneSignal
ONESIGNAL_APP_ID="your_onesignal_app_id"
ONESIGNAL_APP_API_KEY="your_onesignal_app_api_key" # used in: Authorization: Key <...>

Server code (Express)

import express from "express";
import crypto from "crypto";

type SahhaArchetypeEvent = {
  id: string;
  profileId: string;
  accountId: string;
  externalId: string;
  name: string; // e.g. "chronotype"
  value: string; // e.g. "early_bird"
  dataType: string;
  periodicity: "daily" | "weekly" | "monthly";
  ordinality: number;
  startDateTime: string;
  endDateTime: string;
  createdAtUtc: string;
  version: number;
};

const app = express();

/**
 * IMPORTANT:
 * Use raw text body so the HMAC signature is computed over the exact payload.
 */
app.use("/webhooks/sahha", express.text({ type: "*/*" }));

function timingSafeEqualString(a: string, b: string) {
  const bufA = Buffer.from(a, "utf8");
  const bufB = Buffer.from(b, "utf8");
  if (bufA.length !== bufB.length) return false;
  return crypto.timingSafeEqual(bufA, bufB);
}

function verifySahhaSignature(rawBody: string, signatureHeader: string, secret: string) {
  const computed = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  return timingSafeEqualString(signatureHeader.toLowerCase(), computed.toLowerCase());
}

/**
 * Keep your OneSignal tag budget under control by only syncing archetypes you will actually use.
 * Start small; expand when CX teams demonstrate value.
 */
const ALLOWED_ARCHETYPES = new Set<string>([
  "chronotype",
  "sleep_pattern",
  "sleep_quality",
  "activity_level",
  "primary_exercise_type",
]);

function toUnixSeconds(iso: string): string {
  const ms = Date.parse(iso);
  if (Number.isNaN(ms)) return "";
  return String(Math.floor(ms / 1000));
}

function buildOneSignalTags(payload: SahhaArchetypeEvent): Record<string, string> {
  const tags: Record<string, string> = {};

  // Required: archetype itself
  tags[`sahha_archetype_${payload.name}`] = String(payload.value);

  // Optional metadata (comment out if you want to keep tags minimal)
  tags[`sahha_archetype_${payload.name}_periodicity`] = String(payload.periodicity);
  tags[`sahha_archetype_${payload.name}_window_end`] = toUnixSeconds(payload.endDateTime) || "";

  // Global timestamp for "last time Sahha tags were updated"
  tags["sahha_archetypes_updated_at"] = toUnixSeconds(payload.createdAtUtc) || "";

  // Remove empties (OneSignal expects strings, but empty values are usually not helpful)
  for (const [k, v] of Object.entries(tags)) {
    if (!v) delete tags[k];
  }

  return tags;
}

async function updateOneSignalUserTags(externalId: string, tags: Record<string, string>) {
  const appId = process.env.ONESIGNAL_APP_ID!;
  const apiKey = process.env.ONESIGNAL_APP_API_KEY!;

  const url = `https://api.onesignal.com/apps/${encodeURIComponent(appId)}/users/by/external_id/${encodeURIComponent(externalId)}`;

  const res = await fetch(url, {
    method: "PATCH",
    headers: {
      "Authorization": `Key ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      properties: {
        tags,
      },
    }),
  });

  if (!res.ok) {
    const body = await res.text().catch(() => "");
    const err = new Error(`OneSignal update failed: ${res.status} ${res.statusText} ${body}`);
    (err as any).status = res.status;
    throw err;
  }
}

app.post("/webhooks/sahha", async (req, res) => {
  const rawBody = req.body as string;

  const signature = req.get("X-Signature") || "";
  const externalIdHeader = req.get("X-External-Id") || "";
  const eventType = req.get("X-Event-Type") || "";

  // Fast validation
  if (!signature || !externalIdHeader || !eventType) {
    return res.status(400).json({ error: "Missing required headers" });
  }

  // Verify signature
  const secret = process.env.SAHHA_WEBHOOK_SECRET!;
  const isValid = verifySahhaSignature(rawBody, signature, secret);
  if (!isValid) {
    return res.status(401).json({ error: "Invalid signature" });
  }

  // Acknowledge quickly (recommended). Do heavier work async if needed.
  res.status(200).json({ received: true });

  // Only handle archetype events here
  if (eventType !== "ArchetypeCreatedIntegrationEvent") return;

  let payload: SahhaArchetypeEvent;
  try {
    payload = JSON.parse(rawBody);
  } catch {
    console.error("Invalid JSON payload");
    return;
  }

  // Trust but verify: header externalId should match payload externalId
  const externalId = payload.externalId || externalIdHeader;
  if (!externalId) return;

  // Enforce allowlist to protect OneSignal tag limits
  if (!ALLOWED_ARCHETYPES.has(payload.name)) return;

  const tags = buildOneSignalTags(payload);

  try {
    await updateOneSignalUserTags(externalId, tags);
  } catch (e) {
    console.error("Failed to update OneSignal user tags", e);
  }
});

app.listen(3000, () => {
  console.log("Listening on :3000");
});

Testing & validation checklist

1) Validate the webhook endpoint (Sahha)

  • Use Send Test Event in Sahha webhook settings.
  • Confirm your server returns HTTP 200 quickly.
  • Confirm logs show eventType === ArchetypeCreatedIntegrationEvent.

2) Validate tag updates (OneSignal)

In OneSignal Dashboard:

  • Go to Audience → Users
  • Find a user by external_id
  • Check Tags
  • Confirm keys like:
    • sahha_archetype_chronotype = early_bird

3) Validate segmentation

Create a Segment:

  • Condition: sahha_archetype_chronotype equals early_bird
  • Verify expected users appear.

4) Validate a campaign workflow

Create a Journey or Message:

  • Target your Segment
  • Use tag-driven personalization (optional)
  • Confirm delivery behavior aligns with your channel subscriptions.

Operational guidance

Keep tag footprint small

Because OneSignal enforces tag limits per user and blocks new tags once the limit is reached, start with:

  • 3–6 archetype tags that map directly to segmentation hypotheses
  • One global update timestamp tag

Prefer updating existing keys over adding new ones

If you reuse the same key (e.g., sahha_archetype_chronotype) you’re updating an existing tag, which is less likely to be blocked by tag limits than adding new keys.

Don’t store full profiles in tags

OneSignal tags are for segmentation and personalization, not analytics logs. Keep durable profile history in your database.


Next steps (common extensions)

  • Add a backfill job to sync the latest archetypes for existing users (use Sahha Archetypes API / SDK).
  • Add support for additional Sahha event types (Scores, Biomarkers) using separate allowlists and distinct tag namespaces.
  • Route webhook processing through a queue (SQS/PubSub) if you expect high volume.