Segments API
Create and manage dynamic user segments programmatically.
All endpoints on this page are relative to the base URL: https://api-eu1.joryio.com/api - see API Overview.
Authentication
All requests require API key authentication:
Authorization: Bearer jry_live_your_api_key_here
Content-Type: application/json
Create Segment
Create a new user segment with filters.
Endpoint
POST /segments
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Segment name (max 255 chars) |
description | string | No | Segment description (max 1000 chars) |
filterGroups | array | Yes | Array of filter groups (max 20) |
excludeFilterGroups | array | No | Users matching any of these groups are removed (max 20) |
groupOperator | string | Yes | How to combine groups: AND or OR |
includeCrawlers | boolean | No | Count crawler (bot) profiles as members. Default false - the Audience page hides them too. |
tags | array | No | Tag names for organizing segments |
Filter Structure
Each filter group contains up to 50 filters:
{
filters: [
{
type: 'attribute' | 'default_attribute' | 'event' | 'ecommerce'
| 'behavioral' | 'segment' | 'canvas_execution'
| 'list_membership' | 'channel_subscription' | 'app'
| 'entity' | 'bounce_status' | 'wallet_pass'
| 'warehouse_sync' | 'device' | 'random_bucket'
| 'contact_import' | 'message_activity',
field?: string, // For attribute filters
operator: string, // See Filter Operators below
value?: any, // Comparison value (also carries N for count operators)
eventName?: string, // For event filters
withinDays?: number, // Time window for event filters
startDate?: string, // Absolute window start (ISO 8601, event filters)
endDate?: string, // Absolute window end (ISO 8601, event filters)
segmentId?: string, // For segment filters
listId?: string, // For list_membership filters
channel?: string // For channel_subscription filters
}
],
operator: 'AND' | 'OR'
}
A random_bucket filter compares the contact's stable random bucket number (0-9999, never changes for a person): operator is one of equals, not_equals, less_than, less_than_or_equal, greater_than, greater_than_or_equal, between; value is a whole number from 0 to 9999, or [from, to] for between (both ends included). Any other value is rejected with a 400. See Random bucket number.
A contact_import filter ("Came from import") matches contacts by the contact imports that created or updated them: operator is any_of or none_of, and value is a list of 1 to 100 import ids from GET /users/import/sources. none_of includes contacts that no import touched. An empty list is rejected with a 400. Contacts imported before this feature was released are not marked, so they match only none_of. See Came from import.
A message_activity filter ("Message activity") matches people by what happened to the messages sent to them. operator is the action: received (the message.sent event), delivered, opened, clicked, bounced, unsubscribed or failed. messageSource is campaign (with campaignId), campaign_tag (with tagIds), any_campaign, journey (with canvasId), journey_step (with canvasId and nodeId), journey_tag (with tagIds) or any. messageChannel is any or one of email, sms, push, whatsapp, in_app, webhook, viber - only a channel that reports the action (no opened for sms, push or webhook; bounced is email only). messageWindow is ever, in_last_days or not_in_last_days (value = days, 1 to 3650), between_dates (startDate and endDate), at_least (value = times, at least 1) or at_most (value = times, 0 or more). tagIds is a list of tag ids (from GET /tags), several meaning any of those tags; it matches the campaigns or journeys that carry the tag when the audience is computed, and renaming the tag does not change it. A filter saved with a tag name in tag still works and is stored by id on its next save; a name no tag carries is rejected with a 400. Anything else is rejected with a 400. Example: { "type": "message_activity", "operator": "opened", "messageSource": "any_campaign", "messageChannel": "email", "messageWindow": "in_last_days", "value": 30 }. See Message activity.
opened on email or any follows the opener rule: it also matches a person's click on the email, because a click proves the email was opened, and with at_least / at_most it compares the larger of the open and email-click counts. Bot opens and link-scanner clicks never count.
A canvas_execution filter also takes nodeId together with one canvasId - the person's run reached that step of the journey (any node type) - and the operators in_tagged_canvas / not_in_tagged_canvas with tagIds instead of canvasId: in (or not in) any journey that carries the tag. With nodeId, stepWindow limits it to when the run last entered the step: ever (default), in_last_days (withinDays, 1 to 3650) or between_dates (startDate and endDate). A one-journey operator (in_canvas, not_in_canvas, completed_canvas, exited_canvas) stored without canvasId matches nobody. See Canvas execution.
Example Request - Simple Attribute Filter
curl -X POST https://api-eu1.joryio.com/api/segments \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Premium Users",
"description": "Users on premium plan",
"filterGroups": [
{
"filters": [
{
"type": "attribute",
"field": "plan",
"operator": "equals",
"value": "premium"
}
],
"operator": "AND"
}
],
"groupOperator": "AND"
}'
Example Request - Behavioral Segment
curl -X POST https://api-eu1.joryio.com/api/segments \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Active Trial Users",
"description": "Trial users active in last 7 days",
"filterGroups": [
{
"filters": [
{
"type": "attribute",
"field": "plan",
"operator": "equals",
"value": "trial"
},
{
"type": "event",
"eventName": "Session Started",
"operator": "performed",
"withinDays": 7
}
],
"operator": "AND"
}
],
"groupOperator": "AND"
}'
Example Request - Complex Segment
curl -X POST https://api-eu1.joryio.com/api/segments \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "High-Value At-Risk Users",
"description": "Paid users with high LTV who haven'\''t logged in recently",
"filterGroups": [
{
"filters": [
{
"type": "attribute",
"field": "plan",
"operator": "in",
"value": ["premium", "enterprise"]
},
{
"type": "attribute",
"field": "lifetimeValue",
"operator": "gte",
"value": 500
}
],
"operator": "AND"
},
{
"filters": [
{
"type": "event",
"eventName": "Login",
"operator": "not_performed",
"withinDays": 14
}
],
"operator": "AND"
}
],
"groupOperator": "AND"
}'
Response
The segment object is returned directly (no wrapper envelope). Segment IDs are UUIDs. Member counts are not stored on the segment - use GET /segments/:id/size:
{
"id": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"name": "Premium Users",
"description": "Users on premium plan",
"filterGroups": [
{
"filters": [
{
"type": "attribute",
"field": "plan",
"operator": "equals",
"value": "premium"
}
],
"operator": "AND"
}
],
"excludeFilterGroups": [],
"groupOperator": "AND",
"tags": [],
"status": "active",
"createdAt": "2024-01-20T10:30:00.000Z",
"updatedAt": "2024-01-20T10:30:00.000Z"
}
Get Segment
Retrieve segment details.
Endpoint
GET /segments/:id
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | Segment ID |
Example Request
curl -X GET https://api-eu1.joryio.com/api/segments/3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d \
-H "Authorization: Bearer jry_live_your_api_key"
Response
The segment object, returned directly (for the current member count, call GET /segments/:id/size):
{
"id": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"name": "Premium Users",
"description": "Users on premium plan",
"filterGroups": [...],
"excludeFilterGroups": [],
"groupOperator": "AND",
"tags": [],
"status": "active",
"createdAt": "2024-01-20T10:30:00.000Z",
"updatedAt": "2024-01-20T10:30:00.000Z"
}
List Segments
Get all segments with pagination.
Endpoint
GET /segments
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 100 | Results per page (max 100) |
offset | number | 0 | Number of segments to skip |
q | string | - | Free-text search on segment name |
status | string | - | Filter by status: active or archived |
tags | string | - | Comma-separated tag names |
createdBy | string | - | Comma-separated creator user IDs |
editedBy | string | - | Comma-separated last-editor user IDs |
Example Request
curl -X GET "https://api-eu1.joryio.com/api/segments?limit=50&status=active" \
-H "Authorization: Bearer jry_live_your_api_key"
Response
{
"data": [
{
"id": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"name": "Premium Users",
"status": "active",
"createdAt": "2024-01-20T10:30:00.000Z"
},
{
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"name": "Trial Users",
"status": "active",
"createdAt": "2024-01-19T09:15:00.000Z"
}
],
"pagination": {
"total": 23,
"page": 1,
"limit": 50,
"offset": 0,
"totalPages": 1,
"hasMore": false
}
}
Update Segment
Update segment name, description, or filters. Uses PUT (there is no PATCH route); fields you omit are left unchanged.
Endpoint
PUT /segments/:id
Request Body
{
"name": "Updated Name",
"description": "Updated description",
"filterGroups": [...]
}
Example Request
curl -X PUT https://api-eu1.joryio.com/api/segments/3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d \
-H "Authorization: Bearer jry_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"description": "Premium users who have made at least one purchase"
}'
Response
The updated segment object, returned directly:
{
"id": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"name": "Premium Users",
"description": "Premium users who have made at least one purchase",
"status": "active",
"updatedAt": "2024-01-21T14:30:00.000Z"
}
Archive Segment
Segments cannot be hard-deleted - campaigns and journeys hold segment references, so removal is archive-only. Archived segments stop appearing in active lists and can be restored at any time.
Endpoints
POST /segments/:id/archive
POST /segments/:id/unarchive
Example Request
curl -X POST https://api-eu1.joryio.com/api/segments/3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d/archive \
-H "Authorization: Bearer jry_live_your_api_key"
Response
The segment object with its new status:
{
"id": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"name": "Premium Users",
"status": "archived",
"updatedAt": "2024-01-21T14:30:00.000Z"
}
Notes
- Archiving a segment doesn't delete the users in it
- Active campaigns using this segment will be affected
- Use
POST /segments/:id/unarchiveto restore
Get Segment Users
Get list of users in a segment.
Endpoint
GET /segments/:id/users
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 100 | Number of users to return |
offset | number | 0 | Number of users to skip |
Example Request
curl -X GET "https://api-eu1.joryio.com/api/segments/3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d/users?limit=100" \
-H "Authorization: Bearer jry_live_your_api_key"
Response
A bare JSON array of user documents (no pagination envelope - page with limit / offset):
[
{
"_id": "665f1e2a9b3c4d5e6f7a8b9c",
"externalId": "user_123",
"attributes": {
"plan": "premium",
"signupDate": "2024-01-15"
}
},
{
"_id": "665f1e2a9b3c4d5e6f7a8b9d",
"externalId": "user_456",
"attributes": {
"plan": "premium",
"signupDate": "2024-01-18"
}
}
]
Get Segment Size
Get the current number of users in a segment. By default this is a fast approximate estimate; pass ?exact=true for a precise count. The size number is the only thing that is ever approximate - actual membership and sends are always exact.
Endpoint
GET /segments/:id/size
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
exact | boolean | false | true returns the precise count (slower on large workspaces) |
Example Request
curl -X GET https://api-eu1.joryio.com/api/segments/3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d/size \
-H "Authorization: Bearer jry_live_your_api_key"
Response
{
"segmentId": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"size": 1234,
"approximate": true
}
Keeping a segment ready for reports
Analytics reports that filter by a segment have to work out who is in it first, and hand that list of people to the analytics database along with the query. The list is what limits them: a retention comparison analyses at most 50,000 members of a segment, and takes a random sample when a segment is bigger.
Materializing a segment stores its membership in the analytics database instead, so a report joins to it and no list travels. The limit disappears at any size.
It is off by default and switched on per segment, because the cost is a nightly rebuild and most segments are never used in a report. Each organization has an allowance shared across its workspaces - twenty by default. Ask us to raise it.
The trade is freshness. A materialized segment is rebuilt every night and whenever you ask, so a report reads the membership as of the last rebuild rather than as of this second. Reports say which timestamp they used.
Get materialization status
GET /segments/:id/materialization
curl -X GET https://api-eu1.joryio.com/api/segments/3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d/materialization \
-H "Authorization: Bearer jry_live_your_api_key"
{
"materialized": true,
"refreshedAt": "2026-09-03T03:00:00.000Z",
"memberCount": 482103,
"error": null,
"used": 4,
"quota": 20
}
refreshedAt is null until the first rebuild finishes. error carries why the
last rebuild failed, so a stale snapshot can say why it is stale; the previous
membership keeps serving in the meantime rather than the segment emptying.
quota of -1 means unlimited.
List every materialized segment in the organization
GET /segments/materialization/usage
The allowance, what is spent, and which segments spent it - across every workspace in the organization, which no workspace-scoped view can show.
{
"quota": 20,
"used": 2,
"segments": [
{
"id": "3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d",
"name": "Champions",
"workspaceId": "8c1f0b2a-91d4-4a77-9a3e-2d6f7b5c1e40",
"refreshedAt": "2026-09-03T03:00:00.000Z",
"memberCount": 1200,
"error": null
}
]
}
Turn it on
POST /segments/:id/materialization
The first build runs immediately rather than waiting for the night. Returns the same status object.
Returns 400 when the organization has no free slots, naming the segments that
are using them so you can choose one to turn off.
Turn it off
DELETE /segments/:id/materialization
Deletes the stored membership and frees the slot.
Rebuild now
POST /segments/:id/materialization/refresh
Recomputes membership immediately. Membership is rebuilt whole rather than added to, so people who no longer qualify are removed as well as new ones added - which matters because people leave a segment without doing anything: a buyer drops out of "no purchase in 30 days" purely by the calendar moving.
Testing filters before you use them
A filter set is easy to get subtly wrong, and a wrong one is silent: it returns a smaller audience, not an error. These routes let an integration check its work before a send depends on it.
Preview an Audience
POST /segments/preview
How many contacts a filter set matches, without saving a segment.
{
"filters": { "operator": "AND", "conditions": [
{ "type": "attribute", "field": "country", "operator": "equals", "value": "GR" }
] }
}
Response:
{ "count": 4820 }
Preview Matching Users
POST /segments/preview-users
The same query, returning a sample of the actual contacts so you can eyeball whether they are the people you meant.
Check One User
POST /segments/check-user
Does this contact match this filter set? The per-contact gate an integration needs when it is deciding about one person rather than building an audience.
{ "userId": "user_123", "filters": { "operator": "AND", "conditions": [] } }
Response:
{ "userId": "user_123", "matches": true }
Channel Breakdown
POST /segments/channel-breakdown
How many of the matching contacts are actually reachable on each channel. A segment of 10,000 with 400 phone numbers is not an SMS audience of 10,000, and this is where that becomes visible before you schedule the send.
Response:
{
"email": 9210,
"sms": 400,
"push": 3110,
"whatsapp": 280,
"viber": 95,
"matched": 10000,
"capped": false
}
matched is the population the per-channel counts are a subset of, and capped is true when the segment was too large to resolve exactly (the counts are then of the resolved sample). A channel count is what the send path can actually reach: a subscribed contact with no address, no phone or no registered device is not counted. The phone STOP ledger and the address suppression lists are re-checked at send time, so a real send can only be smaller.
Bulk and exports
Create Segments in Bulk
POST /segments/bulk
Creates several segments in one call - for provisioning a workspace from your own configuration rather than clicking through the UI.
{
"segments": [
{ "name": "Greek customers", "filters": { "operator": "AND", "conditions": [] } },
{ "name": "Lapsed 90d", "filters": { "operator": "AND", "conditions": [] } }
]
}
List Export Jobs
GET /segments/exports
Segment exports run asynchronously and hand back a job id. This lists those jobs with their status and, once finished, the presigned download URL.
Response:
{
"exports": [
{ "id": "exp_9f2", "segmentId": "seg_123", "status": "completed",
"rows": 4820, "url": "https://…", "expiresAt": "2026-08-27T12:00:00Z" }
]
}
Filter Operators
Attribute Operators
| Operator | Description | Example |
|---|---|---|
equals | Exact match | plan equals "premium" |
not_equals | Not equal | plan not_equals "free" |
in | Value in list | plan in ["premium", "enterprise"] |
not_in | Value not in list | plan not_in ["free", "trial"] |
contains | String contains | email contains "@company.com" |
not_contains | String doesn't contain | email not_contains "@competitor.com" |
gt | Greater than | lifetimeValue > 1000 |
gte | Greater than or equal | age >= 18 |
lt | Less than | loginCount < 5 |
lte | Less than or equal | mrr <= 99 |
exists | Field exists | phone exists |
not_exists | Field doesn't exist | referralCode not_exists |
within_next_days | Date within next N days | trialEndsDate within_next_days 7 |
A contact with no value for the field matches the negative operators
(not_equals, not_contains, not_in, not_exists) and none of the
positive ones (equals, contains, in, exists, gt, gte, lt,
lte). See Contacts without a value.
email is compared case-insensitively by equals, not_equals, in and
not_in - [email protected] and [email protected] are the same address.
phone is compared on its digits by the same four operators, so
+972501234567, 972501234567 and +972 50-123-4567 all match the same
contact. Every other field is matched exactly as you give it, including
externalId - your own ids stay case-sensitive.
Event Operators
| Operator | Description | Example |
|---|---|---|
performed | User performed event | Performed "Order Completed" |
not_performed | User didn't perform event | Not performed "Onboarding Completed" |
performed_count_gte | Event count greater than or equal (N in value) | Performed "Login" >= 10 times |
performed_count_lte | Event count less than or equal (N in value) | Performed "Login" <= 5 times |
performed_in_last_days | Performed within the last N days (N in value) | Performed "Login" in last 7 days |
not_performed_in_last_days | Not performed within the last N days (N in value) | No "Login" in last 14 days |
Filter Examples
Attribute Filters
// String matching
{
"type": "attribute",
"field": "email",
"operator": "contains",
"value": "@company.com"
}
// Numeric comparison
{
"type": "attribute",
"field": "lifetimeValue",
"operator": "gte",
"value": 500
}
// Multiple values
{
"type": "attribute",
"field": "plan",
"operator": "in",
"value": ["premium", "enterprise"]
}
// Field exists
{
"type": "attribute",
"field": "phone",
"operator": "exists"
}
// Date within next N days (future dates)
{
"type": "attribute",
"field": "trialEndsDate",
"operator": "within_next_days",
"value": 7
}
Event Filters
// Event performed within timeframe
{
"type": "event",
"eventName": "Order Completed",
"operator": "performed",
"withinDays": 30
}
// Event not performed
{
"type": "event",
"eventName": "Onboarding Completed",
"operator": "not_performed"
}
// Event count (N goes in `value`)
{
"type": "event",
"eventName": "Login",
"operator": "performed_count_gte",
"value": 10,
"withinDays": 30
}
Segment Filters
// User in another segment
{
"type": "segment",
"segmentId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"operator": "in_segment"
}
// User not in another segment
{
"type": "segment",
"segmentId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"operator": "not_in_segment"
}
Common Segment Examples
Trial Conversion Opportunity
{
"name": "Trial Expiring Soon",
"description": "Users whose trial ends in the next 3 days and haven't purchased",
"filterGroups": [
{
"filters": [
{
"type": "attribute",
"field": "plan",
"operator": "equals",
"value": "trial"
},
{
"type": "attribute",
"field": "trialEndsDate",
"operator": "within_next_days",
"value": 3
},
{
"type": "event",
"eventName": "Order Completed",
"operator": "not_performed"
}
],
"operator": "AND"
}
],
"groupOperator": "AND"
}
Power Users
{
"name": "Power Users",
"filterGroups": [
{
"filters": [
{
"type": "event",
"eventName": "Login",
"operator": "performed_count_gte",
"value": 20,
"withinDays": 30
},
{
"type": "event",
"eventName": "Feature Used",
"operator": "performed_count_gte",
"value": 50,
"withinDays": 30
}
],
"operator": "AND"
}
],
"groupOperator": "AND"
}
At-Risk Customers
{
"name": "At-Risk Premium Users",
"filterGroups": [
{
"filters": [
{
"type": "attribute",
"field": "plan",
"operator": "in",
"value": ["premium", "enterprise"]
},
{
"type": "attribute",
"field": "lifetimeValue",
"operator": "gte",
"value": 500
}
],
"operator": "AND"
},
{
"filters": [
{
"type": "event",
"eventName": "Login",
"operator": "not_performed",
"withinDays": 14
}
],
"operator": "AND"
}
],
"groupOperator": "AND"
}
Dynamic Updates
Segments are dynamic: membership is not a stored list - the segment's filters are evaluated against current profile and event data whenever the segment is used (campaign targeting, journey gates, membership checks). There is nothing to refresh or recompute via the API.
Checking Segment Size
# Get current size (approximate by default; add ?exact=true for a precise count)
curl -X GET https://api-eu1.joryio.com/api/segments/3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d/size \
-H "Authorization: Bearer jry_live_your_api_key"
Error Responses
All errors use the standard error body - see API Overview: Error Response.
400 Bad Request - Invalid Filter
{
"statusCode": 400,
"message": "Bad Request Exception",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/segments",
"errors": [
"filterGroups.0.filters.0.operator must be one of the following values: equals, not_equals, contains, ..."
]
}
404 Not Found
{
"statusCode": 404,
"message": "Segment with ID 3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d not found",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/segments/3f9d2c1e-7a54-4b2e-9c1d-8e6f5a4b3c2d"
}
Rate Limits
The Segments API has no fixed per-endpoint rate limits today - see API Overview: Rate Limiting.
Best Practices
1. Keep Segments Focused
Good: Specific, targeted segments
{
"name": "Premium US Users - Active Last 7 Days",
"filters": [...]
}
Bad: Overly broad segments
{
"name": "All Users",
"filters": []
}
2. Use Descriptive Names
Good: Self-explanatory names
- "Trial Users - Expiring This Week"
- "High-Value At-Risk Customers"
- "New Signups - Not Onboarded"
Bad: Unclear names
- "Segment 1"
- "Test"
- "Users ABC"
3. Combine Filters Logically
Use AND for narrowing, OR for broadening:
// AND: Premium users who are active
{
"filters": [
{ "field": "plan", "operator": "equals", "value": "premium" },
{ "eventName": "Login", "operator": "performed", "withinDays": 7 }
],
"operator": "AND"
}
// OR: Users on any paid plan
{
"filters": [
{ "field": "plan", "operator": "equals", "value": "premium" },
{ "field": "plan", "operator": "equals", "value": "enterprise" }
],
"operator": "OR"
}
4. Monitor Segment Size
Track segment size over time:
// Poll segment size
setInterval(async () => {
const { size, approximate } = await fetch(`/segments/${segmentId}/size`).then(r => r.json());
console.log(`Segment size: ${approximate ? '≈' : ''}${size}`);
}, 60000); // Every minute
Next Steps
Deleting vs archiving
POST /segments/{segmentId}/archive
DELETE /segments/{segmentId}
Archive is the default. An archived segment stops appearing in pickers and
in membership lookups, while every campaign and journey already pointing at it
keeps working. Reverse it with POST /segments/{segmentId}/unarchive.
Delete removes it permanently, and only when nothing references it. If a
campaign, journey, in-app campaign or another segment still uses it, the request
returns 409 and names them:
{
"statusCode": 409,
"message": "This segment cannot be deleted because it is still used by campaign \"Winback\", journey \"Onboarding\". Archive it instead - an archived segment stops appearing in pickers and in membership lookups, and everything already pointing at it keeps working."
}
GET /users/{userId}/segments evaluates every ACTIVE segment against the
contact, so a workspace that accumulates segments pays for them on every
membership lookup. Archiving or deleting the ones you no longer use is the fix.