Segments
A segment is a named group of organizations, or of contacts when its member_type is contact. An organization segment holds companies only; a person in it is reached through their organization.
Membership comes from either side and the two coexist:
- hand-picked — a
segment_membersrow withsource: "manual"; - rule-materialised — rows with
source: "rule", written by a recompute.
A recompute only ever inserts and deletes rule rows, so a hand-picked member survives every rule change.
The segment
curl "https://api.revenexx.com/v1/customers/segments?limit=50" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..."
| Field | Meaning |
|---|---|
code | Stable identifier, unique per tenant — what other apps and integrations name the segment by. Lowercase with underscores is the convention. |
labels | Localized display names. Null means a client falls back to showing the code. |
position | Sort order, ascending. |
rules | The selector. Null means the segment is manual-only. |
member_type | What the segment holds: organization (the default) or contact. It can be changed only while the segment has no members; a change after that is 409 member_type_in_use. |
active | false when the merchant retired the segment. Its memberships stay, and it still applies wherever it is named, but it is no longer offered as a choice. Its rules are not recomputed until it is active again: a recompute by hand is refused with 422 segment_inactive, and adding a member by hand with 409 segment_inactive. |
rule_match | all (AND, default) or any (OR). Null means the same as all. |
rules_computed_at | When the rule last finished a complete recompute. Null after a rule change and while a chunked recompute is still running — so it doubles as "are the rule memberships trustworthy right now?". |
A made code. POST /v1/customers/segments needs code or labels, at least one. When code is left out or blank, the app makes one from the labels. It takes the first non-empty label value, in the order the labels object was sent, lowercases it, turns ä, ö, ü and ß into ae, oe, ue and ss, drops other accents, replaces every run of other characters with -, trims the dashes and cuts the result to 60 characters. A made code uses dashes, not the underscores of the convention above. If that code is taken (a segment in the trash counts as taken), the app tries <code>-2, <code>-3 and so on, up to <code>-99. With no code and no usable label, the answer is 400 ("'code' is required when the segment has no name to make one from"). When none of those is free, the answer is also 400 ("no free code could be made from this name — type one"). To get a code with underscores, send code yourself.
GET /v1/customers/segments also filters on active and member_type (exact match). ?active=true is the list a picker should offer.
To retire or reinstate a segment, send active on PUT /v1/customers/segments/{id} (boolean, default true, a partial update like the rest). active: false leaves its memberships as they are and keeps it applying wherever it is named. It is only no longer offered as a choice, and its rules are not recomputed, by the schedule or by hand, until it is active again.
Membership by hand
An inactive segment refuses a member added by hand (409 segment_inactive) until it is active again. Its recompute is refused too (422 segment_inactive), see the active row above.
curl -X POST https://api.revenexx.com/v1/customers/segment_members \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"segment_id":"…","organization_id":"…","source":"manual"}'
DELETE /v1/customers/segment_members/{id} takes a member out again. A contact segment needs contact_id on every member (the person) and refuses an organization segment's member that names one (400 member_type_mismatch). The app fills in organization_id from the person's company, and refuses a person with no company or a different organization_id. GET /v1/customers/segment_members filters on contact_id (exact match).
The rule language
Same language product categories use. An organization segment evaluates it over organization columns, setting:<key> entries, and the organization_metrics projection, so "no order in 365 days" is expressible without joining the orders app. A contact segment evaluates it over the person's columns and organization.<column> (see below).
{
"target": "organizations",
"rule_match": "all",
"conditions": [
{ "field": "revenue_365d", "operator": "gte", "value": 100000 },
{ "field": "status", "operator": "eq", "value": "active" }
]
}
target must match the segment's member type: organizations for an organization segment, contacts for a contact segment. Any other value is rejected. Between 1 and 25 conditions.
Contact segments
A contact segment's target is contacts. Its fields are the person's own: first_name, last_name, email, job_title, role, locale, shop_access (text), and consent_marketing (boolean). A company column is reached as organization.<column>, for example organization.branche; the same column without the prefix is refused with 400. The buying-metrics and setting: fields are organization-only.
Fields
What the organization is — an organizations column: name, status, vat_id, branche, external_team_id. Or a top-level key of the free-form settings blob, as setting:<key>.
What it did — read from the buying-metrics projection:
order_count order_count_30d order_count_90d order_count_365d
revenue_total revenue_30d revenue_90d revenue_365d
avg_order_value avg_order_value_365d
first_order_at last_order_at currency
days_since_last_order
days_since_last_order is virtual: it takes gt/gte/lt/lte only, compares last_order_at against a cut-off computed at evaluation time, and never matches an organization that never ordered. For those, use last_order_at with is_empty.
Operators
eq, neq, gt, gte, lt, lte, in (array), contains, starts_with, ends_with, is_empty, is_not_empty. The last two take no value.
A number or boolean makes a setting: condition compare as JSON, so it only matches values stored as a JSON number or boolean — not the string "true".
branche and ?branche= match exactly and case-sensitively.
Two traps in the metrics
- The projection is materialized. A rule reads what the last refresh stored, not live orders. Refresh before you trust a threshold.
currency_mixed. A company that ordered in more than one currency still has stored sums, but they are not comparable against a threshold. A revenue rule should say so.
Preview before you store
curl -X POST "https://api.revenexx.com/v1/customers/segments/{segment_id}/rules/preview" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{"conditions":[{"field":"revenue_365d","operator":"gte","value":100000}],"target":"organizations"}'
A dry run: how many members the rule would select (organizations, or people in a contact segment), with a handful by name. It writes nothing, and it evaluates the rule in the request body rather than the stored one, so an unsaved rule can be previewed.
One count query for the common single-query rule. any rules and rules repeating a column are combined in the app and capped at 5000 ids — capped: true means count is a lower bound.
Recompute is chunked
curl -X POST "https://api.revenexx.com/v1/customers/segments/{segment_id}/rules/recompute" \
-H "X-Revenexx-Tenant: <TENANT_SLUG>" \
-H "X-Revenexx-Api-Key: rvxk_..." \
-H "Content-Type: application/json" \
-d '{}'
Evaluates segments.rules — not the request body — inserts newly matching members (organizations, or people in a contact segment) as source: "rule", and deletes rule rows that no longer match.
done is true. The run is bounded by a wall-clock budget. While done is false, POST again with the returned cursor. added/removed/processed count this call only. rules_computed_at is stamped only when a pass completes.Omitting cursor resumes an unfinished pass and starts a fresh one after a completed pass; an explicit null always restarts.Changing a rule does not move a single membership. Run the recompute.
POST /v1/customers/segments/rules/recompute-all applies the same sync to every active segment with a non-null rule; inactive and trashed segments are skipped. A failing segment is reported in its own result entry instead of aborting the run; the run shares one budget, so repeat until the top-level done is true.
One segment and one membership by id
| Operation | What it does |
|---|---|
GET /v1/customers/segments/{id} | Reads one segment, including its rules. A segment with no rules is hand-picked and valid. |
GET /v1/customers/segment_members/{id} | Reads one membership, with its source: manual for one somebody put in, rule for one the rule engine matched, sync for one an import from the system that owns the grouping wrote (an ERP price group or customer discount, for example). |
PUT /v1/customers/segment_members/{id} | A partial update, meant for correcting source rather than as the normal path. segment_id and organization_id cannot change (400, field_immutable). Answers 409 when another membership already holds the same segment and contact, or, for a member with no contact_id, the same segment and company. |
Trash and restore
DELETE /v1/customers/segments/{id} moves a segment to the trash. Nothing is refused there: its memberships stay but stop applying, its rules are no longer recomputed, and its code stays reserved. A second DELETE deletes it for good, with its memberships. That is refused with 409 (delete_blocked) while an active price list or promotion still names its code. An app that does not answer blocks too; an app the tenant has not installed blocks nothing. Deleting for good is an administrator's: a buyer's call gets 403 (purge_not_allowed).
POST /v1/customers/segments/{id}/restore brings a segment back from the trash. Restore needs the same right as the delete. Nothing was changed when the segment went to the trash, so its memberships, its market assignment and every price list or promotion that names its code apply again as they did. A segment that is not in the trash is refused with 409, not_in_trash; an id no segment has answers 404.
The first delete fires segment.trashed and a restore segment.restored. segment.deleted fires only once the record is deleted for good. See Deletes and the trash.
POST /v1/customers/segments/trash/empty deletes every segment in the trash for good, oldest first, at most 50 per call. Each one is checked on its own, as a second DELETE would be. A segment that an active price list or promotion still names stays in the trash and is listed in blocked with the reason. Call again while more is true. Emptying the trash is an administrator's right: a buyer's call is refused with 403, purge_not_allowed.
Reading and filtering the trash. GET /v1/customers/segments takes trashed: exclude (the default) leaves trashed segments out, only returns the trash alone and include returns the live and the trashed segments together. Each segment carries the fields below, and each one is also an exact-match filter on the list call. A call that acts for a buyer always behaves as exclude, whatever trashed says. deleted_at and purge_at match only an exact timestamp; there is no range filter, so to see what was trashed in a period, sort with order and page.
| Field | What it holds |
|---|---|
deleted_at | When the segment went to the trash; null while it is live. |
deleted_by | Who moved it there: an e-mail, or system. A name for the trash view, not an identity to check. |
deleted_by_id | The platform subject of that person, the stable id a privacy request pseudonymises. Null for the system. |
purge_at | When the daily purge deletes it for good: deleted_at plus the tenant's trash retention (90 days unless the tenant says otherwise). |
trash_batch | The trash action it went with. A restore brings back exactly the records that carry it. |
purge_blocked_reason | Why the purge could not delete it for good, such as an active price list or promotion that still names its code; null when nothing held it. Cleared by a restore. |
The platform sets these fields when a segment is deleted and clears them when it is restored. They are information to read, so do not send them in a write.
Where to go next
- Organizations — the columns and metrics a rule reads.
- Categories — the same rule language over products.
- Price lists — buyer-scoped pricing, which is organization- and contact-scoped rather than segment-scoped.