Segments

A segment is a saved question about your contacts — “on the pro plan and based in Lagos” — answered fresh every time you ask it.

The rule is written in the same language an automation condition uses. That is deliberate: “plan is pro and seats over ten” is one question, and it should mean the same thing whether it is deciding a branch mid-sequence or deciding an audience. Two rule languages would be two sets of edge cases, and they diverge the first time somebody fixes a bug in only one of them.

Membership is never stored. Every read runs the rule against your contacts as they are at that moment. A segment whose members were a saved list would be silently wrong between refreshes, and nothing about looking at it would tell you which.

Creating one

curl -X POST https://api.quolle.com/v1/segments \\
  -H "Authorization: Bearer qle_your_api_key" \\
  -H "Content-Type: application/json" \\
  -d '{
    "name": "Pro plan, Lagos",
    "rule": {
      "type": "and",
      "rules": [
        { "field": "contact.metadata.plan", "operator": "eq", "value": "pro" },
        { "field": "contact.metadata.city", "operator": "eq", "value": "Lagos" }
      ]
    }
  }'
{
  "id": "7aa6bb3e-d6be-488d-88a4-c86393868542",
  "customerId": "8fd34fb3-5f07-456a-b6ee-0c5b71228f44",
  "name": "Pro plan, Lagos",
  "rule": { "type": "and", "rules": [ ... ] },
  "createdAt": "2026-08-23T11:34:34.172Z",
  "updatedAt": "2026-08-23T11:34:34.172Z",
  "contacts": 42
}

contacts is the size right now, computed as part of the response. “How many people is this?” is the only question anybody has about a segment, and a name without it is a guess.

Trying a rule before you save it

Writing a rule blind, naming it, pointing a campaign at it and only then discovering it matches nobody is the failure this prevents. /preview creates nothing.

curl -X POST https://api.quolle.com/v1/segments/preview \\
  -H "Authorization: Bearer qle_your_api_key" \\
  -H "Content-Type: application/json" \\
  -d '{ "rule": { "field": "contact.metadata.plan", "operator": "eq", "value": "pro" } }'
{
  "total": 42,
  "sample": [
    "ada@example.com",
    "chidi@example.com"
  ]
}

sample is the first five addresses — enough to recognise whether the rule caught the people you meant.

What a segment rule can read

FieldReads
contact.email, contact.firstName, contact.lastName, contact.phoneThe contact’s own fields.
contact.metadata.*Anything you store on the contact, nested as deep as you like.

Operators

eq, neq, gt, gte, lt, lte, contains, starts_with, ends_with, exists, is_empty, is_true, is_false. Rules combine with and and or, nesting up to five deep — the same as a condition step.

Two things a segment cannot test. event.* has no meaning here: there is no triggering event when a segment is evaluated. And engagement.* is about one specific email in one specific run, so “did they open it” has no it to refer to. Both are refused with a 422 rather than quietly evaluating to nothing for everybody.

To segment on something that happened during a sequence, record it: a contact_update step can write metadata.opened_onboarding, and then contact.metadata.* can test it.

contact.unsubscribed is refused too, for a quieter reason: a segment never contains unsubscribed contacts in the first place, so testing it would always give the same answer. The filter is already applied.

Who is in it

curl https://api.quolle.com/v1/segments/<id>/contacts?limit=100 \\
  -H "Authorization: Bearer qle_your_api_key"
{
  "total": 42,
  "contacts": [
    {
      "id": "7516c1e4-3182-4b95-ab5c-23e9c072a3b4",
      "email": "ada@example.com",
      "firstName": "Ada",
      "lastName": null
    }
  ]
}

total is the whole segment; contacts is capped by limit, which defaults to 100 and maxes at 500. The honest answer for a broad rule is “most of your contacts”, and returning fifty thousand rows to prove it helps nobody.

Segments and automations

There is no add_to_segment step, because there is nothing to add anybody to. Membership is worked out from the contact’s own fields every time it is asked, so the way to put somebody in a segment is to make the rule true of them:

{
  "key": "mark_engaged",
  "type": "contact_update",
  "config": { "metadata": { "engaged": "yes" } }
}

A segment testing contact.metadata.engaged now includes them. Sending add_to_segment returns a 422 saying this, rather than succeeding and doing nothing.

Endpoints

Method & pathDescription
GET /v1/segmentsList, each with its current size.
POST /v1/segmentsCreate. name and rule.
PUT /v1/segments/<id>Replace the name and rule.
DELETE /v1/segments/<id>Remove it. Contacts are untouched.
GET /v1/segments/<id>/contactsWho matches right now. limit up to 500.
POST /v1/segments/previewTry a rule without saving it.

Errors

StatusErrorMeaning
404Segment not foundNo segment with that id on your account.
422Segment rule tests “…”, which is not a field it can read…The field is outside contact.*. The message lists what is readable.
422A segment can’t test engagement…Engagement belongs to a run, not a contact.
422A segment can’t test event.*…There is no triggering event when a segment is evaluated.
422A segment never contains unsubscribed contacts…The test is already implied. Drop it.