Skip to content
Sendora Cloud
Create account
Understand

Customers

Per-user profile store (traits, computed fields, lifecycle stage, last-seen) + nested AND/OR audience rules over events ↔ traits joins.

Features

  • **Deterministic identity resolution** — profiles keyed on `(orgId, projectId, externalId)`. SDK `identify(userId, traits)` upserts on externalId. **Honest about scope:** no probabilistic cross-device fingerprint stitching today — externalId match only.
  • **Nested AND/OR audience builder** — groups of conditions on event types + properties + profile traits. No SQL, no warehouse round-trip. Evaluated at query time so membership is real-time.
  • **Audience size estimation** — `GET /audiences/:id` returns `estimatedSize` so you see how big a segment is before you target it.
  • **Arbitrary trait storage** — set any trait via `identify(userId, { ltv: 234, plan: "pro" })` and audience conditions can filter on it. **Honest:** there's no built-in computed-trait engine that calculates `first_seen` / `ltv` / `signin_count` for you — you write the value, we store it.
  • **AI lifecycle classifier** (opt-in via `ai_lifecycle_classifier_enabled`) — nightly cron classifies each profile into `new` / `active` / `at_risk` / `churned` based on event activity. Use as an audience condition.
  • **Anonymous → identified device-takeover** — anon `user_id` retired on signin, push tokens reassigned. No duplicate notifications. Webhook + inline SDK listener for your own mirror tables.
  • **Every module reads this** — Push uses audiences for targeting, Email uses them for segments, Automation uses them as triggers, In-App uses them for visibility, Surveys uses them for audience-trigger config. One identity, multiple surfaces.

Common use cases

  • Replace Segment + mParticle + Customer.io CDP — already the source of truth, no reverse-ETL.
  • B2B + B2C apps that need real-time audience membership in messaging + auth + support, not nightly warehouse syncs.
  • Multi-device consumer products where the SAME externalId is passed via `identify()` across web + iOS + Android — those resolve to one profile. (For probabilistic cross-device match without a shared `externalId`, you'd still need a third-party identity-stitching vendor.)

Key concepts

Trait
Top-level user property — `email`, `plan`, `country`. Updatable via `identify({ traits })` or `update_profile` workflow step.
Audience
Saved query: `event-did-happen × event-didn't-happen × trait-is`. Powers segmentation across Email / Push / SMS / Workflow.
Lifecycle stage
AI-classified or rules-based (new / active / dormant / churned). Toggle per-org.

Profile traits

Identify a user

FREE

Bind the current anonymous identity to a known user_id + email + traits. Future events attach to that profile.

await sendora.identify({
  userId: "u_abc123",
  email: "alice@example.com",
  traits: { plan: "growth", company: "Acme" },
});

Fetch a profile

FREE

Returns the canonical profile + traits + audience memberships + lifecycle stage. Use server-side for personalization.

curl https://api.sendoracloud.com/api/v1/orgs/<ORG_UUID>/profiles/<PROFILE_UUID> \
  -H "x-api-key: pk_prod_…"

Build audiences

STARTER+

Nested AND/OR rules over traits + behavioral events. Evaluated lazily at send time. Visual builder in the dashboard. CRUD: GET/POST /orgs/:orgId/audiences, GET/PATCH/DELETE /orgs/:orgId/audiences/:audienceId.

curl -X POST https://api.sendoracloud.com/api/v1/orgs/<ORG_UUID>/audiences \
  -H "x-api-key: pk_prod_…" \
  -d '{
    "name": "Active Pro users",
    "rules": { "all": [
      { "trait": "plan", "op": "eq", "value": "pro" },
      { "event": "feature_used", "op": "occurred_in_last", "value": "30d" }
    ]}
  }'

GDPR export + delete

FREE

Customer-initiated data export (JSON) + right-to-be-forgotten (cascade delete across all modules). Both endpoints idempotent.

# Profile data export — uses Data-IO module (per-profile + bulk)
curl -X POST https://api.sendoracloud.com/api/v1/orgs/<ORG_UUID>/data-io/export \
  -H "x-api-key: pk_prod_…" \
  -d '{ "module": "profiles", "filter": { "profileId": "<PROFILE_UUID>" } }'

# Right-to-be-forgotten — cascade delete via consent module deletion-request
curl -X POST https://api.sendoracloud.com/api/v1/consent/deletion-request \
  -H "x-api-key: pk_prod_…" \
  -d '{ "userId": "u_abc123", "scope": "all" }'

# Direct profile delete (admin scope)
curl -X DELETE https://api.sendoracloud.com/api/v1/orgs/<ORG_UUID>/profiles/<PROFILE_UUID> \
  -H "x-api-key: pk_prod_…"

Audiences (segments)

List audiences

STARTER+

All audiences in the org (optionally filtered by project). Each row includes the resolved rule tree. Visual builder in dashboard mirrors this shape. Auth: requires sk_* secret key. The Web SDK refuses sk_* keys at init time (browser safety) — call audiences API server-side via @sendoracloud/sdk-web-ssr or a custom server proxy. Browser-side audiences.* calls return 403 scope_required.

// SERVER-SIDE only. The Web SDK refuses sk_* keys; pass via SSR client
// or your own server proxy (browser-side calls return 403).
const list = await sendora.audiences.list();

Create an audience

STARTER+

Nested AND/OR rules over traits + behavioral events. `all` = AND, `any` = OR; either can recurse. Evaluated lazily at send time — no precompute.

const aud = await sendora.audiences.create({
  name: "Active Pro users — last 30d",
  description: "Pro plan + recent feature usage",
  rules: {
    all: [
      { trait: "plan", op: "eq", value: "pro" },
      { event: "feature_used", op: "occurred_in_last", value: "30d" },
      {
        any: [
          { trait: "country", op: "eq", value: "US" },
          { trait: "country", op: "eq", value: "CA" },
        ],
      },
    ],
  },
});

Update an audience

STARTER+

PATCH semantics — only fields you pass are touched. Pass a full new `rules` tree to replace the rule set; partial rule edits aren't supported.

await sendora.audiences.update("<AUDIENCE_UUID>", {
  name: "Active Pro users — extended to 60d",
  rules: {
    all: [
      { trait: "plan", op: "eq", value: "pro" },
      { event: "feature_used", op: "occurred_in_last", value: "60d" },
    ],
  },
});

Delete an audience

STARTER+

Hard delete. Workflows + sends that reference the deleted audience by id continue to run — they simply resolve to zero recipients on next eval. Audit-log entry stamped.

await sendora.audiences.delete("<AUDIENCE_UUID>");

Use an audience in a send

STARTER+

Pass `audienceId` (instead of `userIds`/`tokens`) to push / email / SMS send endpoints. Backend resolves at dispatch time — late-joining members of the audience get the send if dispatch hasn't started; suppressed by quiet hours / freq cap as usual.

curl -X POST https://api.sendoracloud.com/api/v1/orgs/<ORG_UUID>/push/send \
  -H "x-api-key: pk_prod_…" \
  -d '{
    "audienceId": "<AUDIENCE_UUID>",
    "title": "Sale starts now",
    "body": "20% off everything"
  }'

Related