MOLTED EMAIL

Segmentation

Create dynamic contact segments with filter groups, nested logic, and async computation.

Segments let you group contacts based on filters - contact fields, account data, metadata, firmographic attributes, and behavioral events. Segments are computed asynchronously and can be used to target experiments (segmentId). Journeys don't read segments: journey trigger conditions and branch conditions only look at the trigger event's payload.

On api.molted.email, pass tenantId (in the body for writes, in the query string for reads); without it a key call fails with 401 missing_tenant_id. mailbox.molted.email, the CLI and the MCP server fill it in. Writes need manage scope and refuse a mailbox-scoped key.

Create a segment

molted segments create \
  --name "Enterprise customers in US" \
  --filters '{
    "logic": "and",
    "filters": [
      { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
      { "type": "metadata", "field": "region", "operator": "eq", "value": "us-west" },
      { "type": "firmographic", "field": "companySize", "operator": "gte", "value": 500 }
    ]
  }'
FlagTypeRequiredDescription
--namestringYesSegment name.
--filtersJSONYesFilter group as JSON. See filter reference below.
POST https://mailbox.molted.email/v1/segments
curl
curl -X POST https://mailbox.molted.email/v1/segments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Enterprise customers in US",
    "filterGroup": {
      "logic": "and",
      "filters": [
        { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
        { "type": "metadata", "field": "region", "operator": "eq", "value": "us-west" },
        { "type": "firmographic", "field": "companySize", "operator": "gte", "value": 500 }
      ]
    }
  }'
FieldTypeRequiredDescription
namestringYesSegment name.
filterGroupobjectYesFilter group object. See filter reference below.

Create (and update) return the segment row { id, tenantId, name, filterGroup, status, createdAt, updatedAt }. The filter group is stored in the canonical shape at write time. Two shorthands are also accepted and normalized: { "operator": "and", "conditions": [{ "field", "op", "value" }] }, and a single bare filter. A filter without type is inferred: email, name, ownerEmail, lifecycleStage and dealStage become contact_field, anything else metadata.

Get a segment

molted segments get seg_abc123
GET https://mailbox.molted.email/v1/segments/:id
curl
curl "https://mailbox.molted.email/v1/segments/seg_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY"

Get also returns archived segments, and adds computeDurationMs. An unknown id is 404 segment_not_found.

List segments

molted segments list
GET https://mailbox.molted.email/v1/segments
curl
curl "https://mailbox.molted.email/v1/segments" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response

Returns an array of active segments. Archived segments are excluded. contactCount and snapshotVersion are null until the segment's first compute (creating a segment doesn't compute it).

Response
[
  {
    "id": "seg_abc123",
    "tenantId": "tenant_abc123",
    "name": "Enterprise customers in US",
    "filterGroup": { "logic": "and", "filters": [ "..." ] },
    "status": "active",
    "contactCount": 142,
    "snapshotVersion": 3,
    "createdAt": "2026-03-01T12:00:00Z",
    "updatedAt": "2026-03-15T08:30:00Z"
  }
]

If you have no segments yet, the response is an empty array:

Empty response
[]

No segments yet? Create your first segment with molted segments create. See creating segments above for filter syntax and examples.

Update a segment

molted segments update seg_abc123 \
  --name "Enterprise US (updated)" \
  --filters '{
    "logic": "and",
    "filters": [
      { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
      { "type": "metadata", "field": "region", "operator": "in", "value": ["us-west", "us-east"] }
    ]
  }'
FlagTypeRequiredDescription
--namestringNoUpdated segment name.
--filtersJSONNoUpdated filter group as JSON.
PATCH https://mailbox.molted.email/v1/segments/:id
curl
curl -X PATCH https://mailbox.molted.email/v1/segments/seg_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Enterprise US (updated)",
    "filterGroup": {
      "logic": "and",
      "filters": [
        { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
        { "type": "metadata", "field": "region", "operator": "in", "value": ["us-west", "us-east"] }
      ]
    }
  }'

Archive a segment

molted segments archive seg_abc123
DELETE https://mailbox.molted.email/v1/segments/:id
curl
curl -X DELETE "https://mailbox.molted.email/v1/segments/seg_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY"

Archiving is a soft delete: it returns { "archived": true } (or 404), and the segment stays readable with get.

Compute segment membership

Trigger an async recomputation of the segment's current members. Each computation creates a point-in-time snapshot of matching contacts.

Most flows do not need to call this directly: contacts sync automatically enqueues a recompute for every active segment in the tenant, so segments list / segments get reflect the new contacts shortly after the sync. Use this command when you want to force a refresh without syncing contacts (e.g., after manually editing contact metadata, or to verify a freshly created segment).

Create and update already store the canonical { logic, filters, type, operator } shape. Compute still rewrites a legacy filter_group shape (e.g. { operator, conditions, op } from older API versions) to the canonical one, as a safety net for rows written before that change.

The compute call returns { "queued": true, "segmentId": "..." }.

molted segments compute seg_abc123
POST https://mailbox.molted.email/v1/segments/:id/compute
curl
curl -X POST "https://mailbox.molted.email/v1/segments/seg_abc123/compute" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'

List segment members

Results are paginated. Use limit and offset to page through members. Each member is { id, email, name, lifecycleStage, dealStage }, ordered by email.

molted segments members seg_abc123 --limit 50 --offset 0
FlagTypeRequiredDescription
--limitnumberNoMax results per page (default 50).
--offsetnumberNoPagination offset (default 0).
GET https://mailbox.molted.email/v1/segments/:id/members
curl
curl "https://mailbox.molted.email/v1/segments/seg_abc123/members?limit=50&offset=0" \
  -H "Authorization: Bearer YOUR_API_KEY"

Get contact segments

List all segments a contact belongs to. This is a reverse lookup -- given a contact, it returns every segment they are a member of.

To check whether a contact is in one specific segment instead, use analytics segment-check.

Returns [{ id, name, status }] for active segments only, or 404 contact_not_found.

molted segments contact con_abc123
GET https://mailbox.molted.email/v1/segments/contact/:contactId/segments
curl
curl "https://mailbox.molted.email/v1/segments/contact/con_abc123/segments" \
  -H "Authorization: Bearer YOUR_API_KEY"

Filter types

Each filter has a type that determines which fields you can query.

TypeDescriptionQueryable fields
contact_fieldFilter on contact properties.email, name, ownerEmail, lifecycleStage, dealStage
account_fieldFilter on the contact's linked account.name, domain, plus any key in the account's metadata object
metadataFilter on contact custom metadata.Any key in the contact's metadata object
firmographicFilter on account custom metadata.Any key in the account's metadata object
behavioralFilter on event counts within a time window.Uses behavioralWindow - see behavioral filters below

Standard filter structure

{
  "type": "contact_field",
  "field": "lifecycleStage",
  "operator": "eq",
  "value": "customer"
}
FieldTypeRequiredDescription
typestringYesOne of: contact_field, account_field, metadata, firmographic, behavioral.
fieldstringYesThe field to filter on. Valid fields depend on the type.
operatorstringYesComparison operator. See operators below.
valueanyVariesThe value to compare against. Not required for exists/not_exists.

Behavioral filters

Behavioral filters count how many times an event occurred within a time window. They use a behavioralWindow object instead of a simple value.

{
  "type": "behavioral",
  "field": "event_count",
  "operator": "gte",
  "value": 0,
  "behavioralWindow": {
    "eventName": "email_opened",
    "windowDays": 30,
    "countOperator": "gte",
    "countValue": 3
  }
}
FieldTypeRequiredDescription
behavioralWindow.eventNamestringYesThe ingested event name to count (e.g. email_opened).
behavioralWindow.windowDaysnumberYesLookback window in days. Stored but not applied yet: counts always cover the last 90 days.
behavioralWindow.countOperatorstringYesOperator to apply to the event count (e.g. gte, eq, lt).
behavioralWindow.countValuenumberYesThe threshold count to compare against.

This example matches contacts with 3 or more email_opened events in the window.

Behavioral counts only see events you ingest yourself with POST /v1/agent/events/ingest (molted journeys trigger). Sends, opens and clicks are not recorded there automatically, so email_opened-style names only match if you ingest events with those names. Two more limits today: windowDays is ignored and counts always cover the last 90 days, and a behavioral filter's own field, operator and value are ignored (only behavioralWindow is evaluated).

Operators

OperatorValue typeDescriptionExample value
eqanyEquals."pro", 500, true
neqanyNot equals."free"
gtnumberGreater than.100
gtenumberGreater than or equal.500
ltnumberLess than.50
ltenumberLess than or equal.1000
containsstringString contains substring."example.com"
not_containsstringString does not contain substring."test"
inarrayValue is in the provided list.["us-west", "us-east"]
not_inarrayValue is not in the provided list.["spam", "bounced"]
existsnoneField exists and is not null. No value needed.-
not_existsnoneField does not exist or is null. No value needed.-
betweenarrayValue is between two bounds (inclusive).[100, 500]

Type constraints: gt, gte, lt, lte, and between only work with numeric values. contains and not_contains only work with strings. Using an operator with the wrong value type returns no matches.

Filter group structure

A filter group wraps one or more filters with a logic operator.

{
  "logic": "and",
  "filters": [
    { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
    { "type": "metadata", "field": "region", "operator": "eq", "value": "us-west" }
  ]
}
FieldTypeRequiredDescription
logicstringYesand or or. Determines how filters are combined.
filtersarrayYesArray of filters or nested filter groups.

Nested logic

Filter groups can contain other filter groups for complex AND/OR logic:

Nested filter group
{
  "logic": "or",
  "filters": [
    {
      "logic": "and",
      "filters": [
        { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "lead" },
        {
          "type": "behavioral",
          "field": "event_count",
          "operator": "gte",
          "value": 0,
          "behavioralWindow": {
            "eventName": "email_opened",
            "windowDays": 30,
            "countOperator": "gte",
            "countValue": 3
          }
        }
      ]
    },
    {
      "logic": "and",
      "filters": [
        { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
        { "type": "metadata", "field": "plan", "operator": "eq", "value": "enterprise" }
      ]
    }
  ]
}

This matches contacts who are either (leads with 3+ email opens in the last 30 days) OR (enterprise customers). Nesting depth is unlimited.

  • Contacts - manage the contacts that segments filter on
  • Analytics - check segment membership with segment-check
  • Experimentation - target an experiment at a segment with segmentId

MCP tools

The segment tools are in the opt-in journeys toolset (see MCP server):

ToolParameters
create_segmentname, filter_group (logic, flat filters[]; no nesting and no behavioral_window, use update_segment for those)
get_segmentsegment_id
update_segmentsegment_id, name, filter_group (supports behavioral_window: event_name, window_days, count_operator, count_value)
archive_segmentsegment_id
compute_segmentsegment_id
check_membershipsegment_id, contact_email
segment_memberssegment_id, limit (1-500), offset
contact_segmentscontact_id

There is no list tool; read the molted://segments resource instead.