Documentation menu

Customer groups

Customer groups are accounts inside one tracked application and environment. They never grant console access or authorize actions in your application. Open People → Groups to manage profiles, membership, account activity and ordered group funnels.

Establish identities and membership

Keep a personal Management token with write scope, people.write and people.pii.read on your server. Organization tokens retain the frozen developer role and cannot write group profiles or read their PII. Read calls require read scope and people.read; group analytics also requires analytics.read and both People and Analytics modules. Public and secret ingest keys cannot change authoritative group profiles or membership. Group keys, names, properties and member labels are hidden from readers without people.pii.read.

import { SaaSProMax } from "@saaspro/node";
import { GroupClient } from "@saaspro/node/groups";

const spm = new SaaSProMax({ secretKey, host });
const groups = new GroupClient({ token, appId, host, environment: "production" });
const group = await groups.command({
  action: "create", type: "company", key: "acme", name: "Acme",
  properties: { plan: "pro", seats: 12 }, reason: "Provision the Acme customer account",
});
spm.identify({ distinctId: "user-42" });
await spm.flush();
const membership = await groups.command({
  action: "member", id: group.id, revision: group.revision,
  distinctId: "user-42", present: true, reason: "Add the confirmed account member",
});
spm.track({ event: "Project created", distinctId: "user-42", context: { groups: { company: "acme" } } });
await spm.flush();
groups.close();
await spm.shutdown();

The Management endpoint is POST /api/v1/apps/{appId}/groups?env=production. Send a command with action: create, update, member, archive or delete. Every command requires a reason of 10–500 characters; all commands except create require the current id and positive revision. Updates replace name and properties. Membership takes an already-ingested distinctId and present boolean. Archive takes archived; delete removes the group profile, memberships and group references in retained events, while leaving individual events intact. GET the same collection or /groups/{id} to read the current revision. A duplicate scoped type/key or stale revision receives 409. Do not blindly retry an uncertain write: read the profile and membership first.

Each environment supports five group types, 10,000 profiles and 10,000 members per group. Types are lower-case identifiers up to 32 characters; external keys are opaque strings up to 128 characters. Properties are a JSON object limited to 8 KiB, 100 top-level keys, eight levels and 2,000 nodes. Never put credentials in properties. Console writes use the same transactional service with CSRF, same-origin checks and a passkey verified within ten minutes. All writes append audit atomically. Management tokens remain subject to current membership, role, module and environment checks.

The optional Node client permits four concurrent calls, uses a bounded timeout (five seconds by default, 100 ms–30 s), accepts at most 16 KiB per request, bounds responses to 1 MiB, refuses redirects and cancels outstanding work on close(). It deliberately does not retry writes. Errors report status and recovery guidance without echoing tokens or response bodies. The server is authoritative for input validation and revisions. Group commands and analytics batches serialize within one application across environments so mixed identity merges cannot race membership changes; unrelated applications remain independent.

Attribute events explicitly

Choose at most one group per type and five types per event. Selection is per call; the SDK does not persist a current account or silently attach it after an identity switch.

// Browser: follow the application's existing consent and identify lifecycle.
spm.track("Project created", { template: "basic" }, { groups: { company: "acme" } });
from saaspro import SaaSProMax

with SaaSProMax(secret_key=secret_key, host=host) as spm:
    spm.track("Project created", distinct_id="user-42",
              context={"groups": {"company": "acme"}})

Group selection travels as optional context.groups; existing SDK event shapes remain compatible. Python and Node accept it through their existing event context. The browser adds a bounded server-validated groups track option and omits it in ephemeral mode. Consent, reset and queue-discard behavior remain owned by the core SDK; no group identifier is stored in browser persistence.

The server binds a selection only if the person identity already resolves to a current group member, the group is active in the ingest key's environment, and membership began no later than the event timestamp. Unknown group keys, late identities, missing membership, archived groups and a disabled People module leave selections unbound. Unbound counts describe requested selections, not rejected events. They remain valid individual events. Buffered events from before membership are not assigned retroactively, and previously ingested events are never backfilled. Ingest order and identity delivery therefore affect group coverage: flush identify, await membership confirmation, then send group events.

Event snapshots store internal group IDs, not copied customer properties or raw external keys. Membership removal and profile edits preserve previous event attribution. Archive stops new attribution and flag eligibility while preserving history. Identity aliases merge current membership into the surviving person; deleting a person deletes their current memberships. Group links disappear with normal event deletion/partition retention. Analytical reads apply the application retention limit immediately, including before physical cleanup. Profiles persist until explicitly deleted or their application/environment is removed. Group deletion serializes against new attribution and scrubs existing retained event references transactionally; a query-budget failure rolls back the entire deletion.

Measure account conversion

Group activity reports unique active groups, attributed event counts, unbound selections and daily group uniqueness for the selected environment. The directory is paginated at 100 rows. The member list shows the latest 100; the full current count is explicit.

A funnel uses groups, rather than individual people, as its unit. Select one group type, two to five exact track-event names, 1–90 lookback days and a 1–720 hour conversion window. Different members may complete consecutive steps for the same group. Each group enters at its first retained matching event in the requested range; subsequent steps must follow in timestamp/event-ID order and fit within the first-step window. Repeated attempts do not restart that window, and a single event cannot complete two repeated steps. Events without stored group links never enter a group funnel. Reports use a bounded SQL query and UTC timestamps; shortened ranges remain available when the query budget is exceeded.

const funnel = await groups.funnel({
  type: "company", steps: ["Project created", "Teammate invited", "Export completed"],
  days: 30, windowHours: 168,
});

The corresponding read-scoped endpoint is POST /api/v1/apps/{appId}/groups/analytics/funnel?env=. Activity is GET /groups/analytics/activity. These are observed product metrics; they do not establish billing status, access rights or randomized experiment results.

Target flags by group

Open a flag's Delivery tab and choose a customer group type. Optionally restrict it to up to ten groups; no group selection includes every current member group of that type in its own environment. The console lists at most 200 profile choices, including current selections, while every active group type remains available. Group target configuration is shared across environments, with each selected ID belonging to its displayed environment. Existing People cohort filters still apply first.

const context = {
  distinctId: "user-42",
  properties: { $groups: { company: "acme" } },
};
const result = await localFlags.resolve(context, { keys: ["shared-checkout"] });
const bootstrap = await localFlags.bootstrapResolved(context, {
  publicKey, keys: ["shared-checkout"], consent: "granted",
});

properties.$groups is only a selector. The server verifies current membership and uses stored group properties for all rules; caller-supplied properties cannot change them. Condition plan reads the stored group plan. Rollout and variant buckets use the internal group identity, giving eligible members the same result. Individual identity overrides are ignored for group-targeted flags. A missing, archived, deleted or unselected group returns false remotely. These controls are presentation flags and must not replace application authorization.

Group-dependent flags remain remote-only. Local synchronous calls use the caller fallback and expose remoteOnlyKeys; ordinary flags continue to evaluate locally. The version 1 snapshot retains its existing reason: "cohort" audience discriminator and adds optional dependency: "group", preserving older P13 parsers. Group IDs, membership and properties never enter snapshots or SSR bootstrap. Existing context hashes include the $groups selector, so switching groups invalidates the old bootstrap. Use freshly resolved values for SSR; do not put private targeting properties in browser scope. Default remote SDK caches can retain answers for 30 seconds, and a fresh bootstrap expires within 30 seconds. Use direct remote resolution when that delay is unacceptable. No group evaluation records experiment exposure or modifies an experiment design.