diff --git a/docs/content/docs/api/endpoints.mdx b/docs/content/docs/api/endpoints.mdx index 7acc0d80..09991215 100644 --- a/docs/content/docs/api/endpoints.mdx +++ b/docs/content/docs/api/endpoints.mdx @@ -82,6 +82,7 @@ All paths below are relative to the versioned base URL `https://api.warmbly.com/ | GET | `/contacts/:id/emails` | `READ_CONTACTS` | | GET | `/contacts/:id/timeline` | `READ_CONTACTS` | | GET | `/contacts/:id/campaigns` | `READ_CONTACTS` | +| GET | `/contacts/:id/segments` | `READ_CONTACTS` | | POST | `/contacts/export` | `READ_CONTACTS` | | POST | `/contacts/import/preview` | `WRITE_CONTACTS` | | POST | `/contacts/import/commit` | `BULK_CONTACTS` | @@ -101,6 +102,24 @@ The import pair is two steps over the same file: preview parses it and suggests AI contact research charges credits (2 per run, billable even when it finds nothing) and only saves cited findings. See the [AI contact research](/guides/ai-contact-research/) guide. The batch endpoint accepts up to 500 contact ids and drains in the background. +### Segments + +| Method | Path | API Permission | +|--------|------|----------------| +| GET | `/segments` | `READ_CONTACTS` | +| GET | `/segments/fields` | `READ_CONTACTS` | +| POST | `/segments/preview` | `READ_CONTACTS` | +| POST | `/segments` | `WRITE_CONTACTS` | +| GET | `/segments/:id` | `READ_CONTACTS` | +| PATCH | `/segments/:id` | `WRITE_CONTACTS` | +| DELETE | `/segments/:id` | `WRITE_CONTACTS` | +| POST | `/segments/:id/members` | `WRITE_CONTACTS` | +| POST | `/segments/:id/members/lookup` | `READ_CONTACTS` | +| GET | `/segments/:id/overrides` | `READ_CONTACTS` | +| POST | `/segments/:id/add-to-campaign` | `WRITE_CAMPAIGNS` | + +Segments are saved contact audiences: a condition list plus per-contact manual overrides, evaluated live. Contact scopes cover them because a segment is a view over contacts; enrolling one into a campaign writes leads, so that call takes the campaign write scope. `POST /contacts/search` and `POST /contacts/export` accept `segment_ids` to scope any contact query to a segment. See [contacts](/api/reference/contacts/#segments) for the condition format. + ### Lead sync Saved Google Sheets sources that upsert contacts on demand. Gated under the contact write scope because a sync ultimately writes contacts. Nothing syncs on a timer: a source only runs when `/lead-sync/sources/:id/sync` is called. diff --git a/docs/content/docs/api/reference/campaigns.mdx b/docs/content/docs/api/reference/campaigns.mdx index 10249756..7a8f10c7 100644 --- a/docs/content/docs/api/reference/campaigns.mdx +++ b/docs/content/docs/api/reference/campaigns.mdx @@ -874,7 +874,7 @@ All fields optional. | `wait_after` | integer | no | Days to wait before this step, counted from the contact's previous step (`0` to `60`). Spacing belongs to the target step, so there is no standalone wait node for email steps. | | `conditions` | object | no | The connections out of this step (`{branches: [...]}`), evaluated in order; a branch with no `conditions` is a plain "go there next" link. Routing follows connections only: a step with `{}` or no branches has no outgoing path and ends the flow for the contact. | | `kind` | string | no | `email` (default), `action`, or `wait`. | -| `action` | object | no | Typed config for non-email nodes. `type` is the switch (`wait`, `add_tag`, `remove_tag`, `unsubscribe`, `notify`, `create_task`, `create_deal`, `move_deal_stage`, `run_automation`, `end`), the remaining fields are type-scoped. | +| `action` | object | no | Typed config for non-email nodes. `type` is the switch (`wait`, `add_tag`, `remove_tag`, `add_to_segment`, `remove_from_segment` (each with a `segment_id`), `unsubscribe`, `notify`, `create_task`, `create_deal`, `move_deal_stage`, `run_automation`, `end`), the remaining fields are type-scoped. | ```json { diff --git a/docs/content/docs/api/reference/contacts.mdx b/docs/content/docs/api/reference/contacts.mdx index b9e9f61e..40cad460 100644 --- a/docs/content/docs/api/reference/contacts.mdx +++ b/docs/content/docs/api/reference/contacts.mdx @@ -31,6 +31,7 @@ Every field is optional; an empty body matches all contacts in the organization. | `lead_status` | string | No | Filter to one derived lead status: `pending`, `active`, `completed`, `replied`, `bounced`, `failed`, `undeliverable`, or `unsubscribed`. Requires exactly one `campaign_ids` entry, otherwise the request is rejected with `lead_filter_requires_campaign`; an unknown value is rejected with `invalid_lead_status`. | | `engagement` | string | No | Filter by engagement inside that campaign: `opened`, `not_opened`, `clicked`, `not_clicked`, `replied`, `not_replied`, or `bounced`. `opened` means a human open (machine opens never count); the `not_*` values match only leads sent at least one step. Combines with `lead_status` as AND. Requires exactly one `campaign_ids` entry (`lead_filter_requires_campaign`); an unknown value is rejected with `invalid_engagement`. | | `category_ids` | string[] | No | Contact must have ALL of these categories. | +| `segment_ids` | string[] | No | Contact must be a member of ALL of these segments (conditions plus manual overrides). An id that is not a valid UUID is rejected with `400`; an unknown segment matches nothing. | | `min_campaigns` | integer | No | Minimum number of associated campaigns. | | `max_campaigns` | integer | No | Maximum number of associated campaigns. | | `subscribed` | boolean | No | Filter by subscription status. | @@ -927,3 +928,95 @@ Auth: **Scope** `READ_CRM` · **Org permission** `view_contacts` ``` `status` is `open`, `won`, or `lost`. `value`, `expected_close_date`, `won_at`, `lost_at`, `lost_reason`, `assigned_to`, `campaign_id`, and `source_mailbox_id` are nullable and omitted when unset. + +## Segments + +Segments are saved contact audiences: a list of conditions plus per-contact manual overrides. Membership is evaluated live on every read, so a segment never needs rebuilding. Every segment endpoint takes the contact scopes, except enrolling into a campaign, which writes leads and takes `WRITE_CAMPAIGNS`. + +A segment object: + +```json +{ + "id": "0b6f9c3e-2f7a-4c0e-9d8e-1a2b3c4d5e6f", + "organization_id": "…", + "name": "Warm fintech leads", + "description": "Opened in the last 30 days, not yet replied", + "color": "#0284c7", + "match": "all", + "conditions": [ + { "field": "custom.industry", "operator": "equals", "value": "fintech" }, + { "field": "last_opened_at", "operator": "within_days", "value": "30" }, + { "field": "emails_replied", "operator": "equals", "value": "0" } + ], + "contact_count": 412, + "included_count": 3, + "excluded_count": 1, + "created_at": "2026-08-01T09:12:00Z", + "updated_at": "2026-08-20T14:03:00Z" +} +``` + +`match` is `all` or `any`. A contact is a member when it matches the conditions or is manually included, and is not manually excluded. A segment with no conditions holds only its manual includes. + +### Conditions + +Each condition names a `field`, an `operator`, and either a `value` (scalar operators) or `values` (list operators). Fields and their kinds are returned by `GET /segments/fields`, including the workspace's custom fields as `custom.` (written with the key in place of the angle-bracket placeholder, for example `custom.industry`). + +| Kind | Fields | Operators | Value | +| --- | --- | --- | --- | +| text | `first_name`, `last_name`, `email`, `email_domain`, `phone`, `company`, `custom.*` | `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty` | `value` string; comparisons ignore case | +| enum | `source`, `verification_status`, `esp_provider` | `in`, `not_in` | `values`, drawn from the field's `options` | +| bool | `subscribed`, `suppressed`, `is_catch_all` | `is_true`, `is_false` | none | +| date | `created_at`, `updated_at`, `last_sent_at`, `last_opened_at`, `last_clicked_at`, `last_replied_at` | `within_days`, `not_within_days` (`value` is a day count, 1 to 3650); `before`, `after` (`value` is `YYYY-MM-DD` or RFC 3339); `is_empty`, `is_not_empty` | see operators | +| number | `campaign_count`, `emails_sent`, `emails_opened`, `emails_clicked`, `emails_replied`, `emails_bounced` | `equals`, `not_equals`, `gt`, `gte`, `lt`, `lte` | `value`, a whole number | +| category | `category` | `in`, `not_in`, `is_empty`, `is_not_empty` | `values`, category ids | +| campaign | `campaign` | `in`, `not_in`, `is_empty`, `is_not_empty` | `values`, campaign ids | +| segment | `segment` | `in`, `not_in` | `values`, segment ids; at most five levels deep, no loops | + +Engagement counters add up every campaign the contact has been in, and opens count human opens only. Limits: 50 conditions per segment, 200 values per list condition, 200 segments per workspace. A condition that fails validation is rejected with `400` and a message naming the condition. + +### List, create, read, update, delete + +`GET /segments` returns every segment with live counts under `data`. `POST /segments` creates one from `name` (required), `description`, `color` (`#rrggbb`), `match` and `conditions`; a duplicate name is a `409`. `GET /segments/:id` returns one segment. `PATCH /segments/:id` accepts the same fields, all optional. `DELETE /segments/:id` returns `204`, or `409` when another segment's conditions reference it. + +Auth: **Scope** `READ_CONTACTS` for reads, `WRITE_CONTACTS` for writes · **Org permission** `view_contacts` / `manage_contacts` + +### Preview a definition + +`POST /segments/preview` + +Counts the contacts an unsaved definition would match. Send `match` and `conditions`; include `id` to keep that segment's manual overrides in the count while editing it. + +```json +{ "contact_count": 412 } +``` + +### Manual overrides + +`POST /segments/:id/members` + +```json +{ "contacts": ["…", "…"], "mode": "include" } +``` + +`mode` is `include` (pin in), `exclude` (pin out) or `auto` (clear the override). Up to 1,000 contact ids per call; ids outside the organization are ignored. Returns `{ "updated": n }`. + +`POST /segments/:id/members/lookup` takes `{ "contacts": [...] }` and returns `{ "data": { "": "include" | "exclude" } }` for the contacts that carry an override. + +`GET /segments/:id/overrides` lists every pinned contact (`contact_id`, `first_name`, `last_name`, `email`, `company`, `mode`, `created_at`) under `data`, includes first, newest first, capped at 500. + +`GET /contacts/:id/segments` is the contact-side view: every segment in the organization with `member` (whether the contact is in it right now) and `mode` (its override, when any) under `data`. + +Sequence action steps `add_to_segment` and `remove_from_segment` take a `segment_id` and apply the include or exclude override to the contact when the step runs; see [campaigns](/api/reference/campaigns/). + +### Add to a campaign + +`POST /segments/:id/add-to-campaign` + +```json +{ "campaign_id": "…" } +``` + +Enrols every current member as a lead. Contacts already in the campaign are skipped, each new lead gets a `campaign_added` activity, and a running campaign is woken so the leads are scheduled. Returns `{ "campaign_id", "added", "members" }`. This is a snapshot: later members are not added until the call is repeated. Safe to retry. + +Auth: **Scope** `WRITE_CAMPAIGNS` · **Org permission** `manage_campaigns` diff --git a/docs/content/docs/guides/workspace-export-import.mdx b/docs/content/docs/guides/workspace-export-import.mdx index 5e454b86..90e97790 100644 --- a/docs/content/docs/guides/workspace-export-import.mdx +++ b/docs/content/docs/guides/workspace-export-import.mdx @@ -14,7 +14,7 @@ The data is split into groups. Every export includes **Workspace**; the rest are | Group | Contents | |-------|----------| | Workspace | The organization, members, roles, teams, mailboxes, API keys, webhooks, and settings, including the website tracking site key. Always included | -| Contacts | Contacts, categories, notes, activities, and the suppression list | +| Contacts | Contacts, categories, segments with their manual overrides, notes, activities, and the suppression list | | Campaigns | Campaigns, sequences, senders, attachments, and per-campaign settings | | CRM | Pipelines, deals, tasks, and meeting bookings | | Automations | Automations, connected integrations, and lead sync sources |