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
FREEBind 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
FREEReturns 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
FREECustomer-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"
}'