Tags
Create tags, list them with live counts, and apply or remove one from up to a thousand contacts at once, by handle or by a saved segment.
Tags are the same ad-hoc labels the dashboard's Signups table applies, first-class, renameable,
colored, shared with segments (a tag condition filters on them) and with the "Add tag" / "Has a
tag" automation steps. Nothing about tagging is API-specific.
Scopes#
| Operation | Scope |
|---|---|
| List tags | contacts:read |
| Create, apply, remove | contacts:write |
List tags#
GET /v1/tags
Newest first, cursor-paginated (pagination). Each tag carries
contacts_count, an index-backed count of how many contacts currently hold it, cheap even on a
large audience because it never scans the contacts table itself.
| Parameter | Type | Description |
|---|---|---|
project |
string | Required. A prj_ handle. |
limit |
integer | 1 to 100, default 25 |
starting_after |
string | Page forward from this tag id |
ending_before |
string | Page backward |
Create a tag#
POST /v1/tags
Idempotent by name within the project: creating a tag that already exists by that name returns the existing one (200) instead of a duplicate, so you can call this before every apply without a prior lookup. A brand new tag answers 201.
| Parameter | Type | Required | Description |
|---|---|---|---|
project |
string | Yes | A prj_ handle. |
name |
string | Yes | Up to 60 characters. |
color |
string | No | A hex color, up to 32 characters. |
Apply or remove a tag#
POST /v1/tags/{tag}/contacts
DELETE /v1/tags/{tag}/contacts
{tag} is the tag_… handle from a list or create call. Both verbs take the identical body: an
explicit list of contact handles, or a saved segment. Send exactly one of contacts / segment,
never both, and never neither: either failure is a 422.
| Parameter | Type | Description |
|---|---|---|
contacts |
array of string | con_… handles or uuids, 1 to 1000. Mutually exclusive with segment. |
segment |
string | A seg_… handle. Every contact it currently matches is affected, unless limit narrows it. |
limit |
integer | With segment only, 1 to 1000. Affects the oldest matching contacts first, by signup date. |
Both responses are a tag_apply_result: the tag (with its refreshed contacts_count), matched
(how many contacts the request resolved to before applying anything), and attached (on the add
call) or detached (on the remove call), how many rows actually changed. A contact that already
carried the tag (or never did, on remove) is left alone and does not count toward that number.
Nothing else happens: no webhook, no automation, no email. Contacts held past the workspace's plan limit are silently excluded from both paths, the same as every other bulk action in the product.
Next#
- Segments, build the filter a
segmentargument points at - Pagination
Common questions
What scope does tagging need?
contacts:read to list tags, contacts:write to create one or apply/remove it. A tag is audience metadata, not a resource with its own blast radius, so it does not carry a scope of its own.
Does tagging send email or fire an automation?
No. Tagging carries no side effects today, over the dashboard or the API: no webhook, no automation trigger, no email.
How do I "tag the next 20 people who don't have this tag yet"?
Build a segment with a tag condition (operator: not_in, the tag's id) plus whatever else you want, save or preview it, then call POST /tags/{tag}/contacts with segment and limit: 20. The oldest 20 matches, by signup date, get tagged.
Can applying a tag touch contacts held past my plan's limit?
No. Held contacts are excluded from both the explicit-handle and the segment path, exactly like every other bulk action in the product.