> ## Documentation Index
> Fetch the complete documentation index at: https://docs.member.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Tags, attributes and contact engagement

> Inspect tag usage, combine duplicate tags, find attribute settings across hubs, and read a contact's email engagement.

Use these routes to see who has a tag, combine duplicate audience labels, find
where a custom field is configured, or review email engagement for one contact.
Tags are team-scoped labels. Contact attributes are custom fields; an attribute
definition describes the field, while each hub can configure how it appears.
A team contact is a contact's association with your team, identified by
`team_contact_id`.

All routes below use this base path:

```text theme={null}
/api/v1/teams/{team_id}
```

Read routes require `Authorization: Bearer <token>` with either the team owner's
JWT or a full-access API key belonging to the same team. A platform user who is
not the owner, a key for another team, or a key without full access receives
`403`. Merging tags requires the owner's JWT; API keys receive `403`, even with
full access. Responses use `application/vnd.api+json`. Send that content type
for the merge request body too.

## Tag usage

Check how many contacts currently have each tag, or inspect one tag:

```text theme={null}
GET /api/v1/teams/{team_id}/tags?page[size]=20
GET /api/v1/teams/{team_id}/tags/{tag_id}
```

Both return `tags` resources with these read-only fields in `attributes`, in
addition to the tag's name, slug, and other details:

| Attribute | Meaning |
| - | - |
| `contact_count` | Number of current assignments to non-deleted team contacts. Deleted team contacts are excluded; hub membership status does not affect the count. |
| `last_assigned_at` | Latest assignment time among those surviving assignments. Ties use assignment ID descending. `null` when there are none. |
| `last_assigned_by_user_id` | User ID recorded on that latest assignment. It can be `null` even when an assignment exists. It is also `null` when there are no assignments. |

These fields describe current assignments, not a history of everyone who ever
had the tag. Removing the latest assignment can make `last_assigned_at` move
back to an older one. An unused tag has `contact_count: 0` and both latest
assignment fields set to `null`.

The tag list is ordered by tag ID ascending. It accepts `page[size]` from `1`
to `100` (default `20`) and uses a tag ID as `page[after]`. Its response has
`meta.has_more` and `links.next`.

## Tag assignments

List the team contacts that currently have a tag:

```text theme={null}
GET /api/v1/teams/{team_id}/tags/{tag_id}/assignments?page[size]=20
```

Each item is a `tag_assignments` resource. Its `id` identifies the assignment,
and its `attributes` contain:

| Attribute | Meaning |
| - | - |
| `team_contact_id` | Team contact holding the tag. |
| `tag_id` | Assigned tag. |
| `assigned_at` | Time the assignment was made. |
| `assigned_by_user_id` | User ID recorded for the assignment, or `null` if none was recorded. |

Only assignments to non-deleted team contacts are included. Results are ordered
by assignment ID ascending, rather than by `assigned_at`. An existing unused
tag returns an empty `data` array. See [pagination and read errors](#pagination-and-read-errors)
for the cursor format and response fields.

## Merge duplicate tags

Combine a source tag into a target tag when both represent the same audience
label. The target keeps its identity and details. Contacts with only the source
receive the target, retaining the original assignment ID, time, and user
attribution. Contacts with both keep their existing target assignment, and the
duplicate source assignment is removed. The source tag is then deleted.

Existing mappings from numeric legacy tag IDs are redirected to the target,
so those imported IDs resolve to the surviving tag. Event RSVP tags that still
point to the source are also redirected, including those on past or cancelled
events. A scheduled event that has not ended can block the merge, as described
below. Deleted events are left out.

Send this request with the team owner's JWT:

```text theme={null}
POST /api/v1/teams/{team_id}/tags/{source_tag_id}/merge
Authorization: Bearer <owner_jwt>
Content-Type: application/vnd.api+json
```

```json theme={null}
{
  "data": {
    "type": "tag_merges",
    "attributes": {
      "target_tag_id": "019a0000-0000-7000-8000-000000000002"
    }
  }
}
```

Use an existing target tag ID from the same team. The body accepts only this
shape; extra fields are rejected. A successful request returns `200` with the
surviving `tags` resource in `data` and these top-level `meta` fields:

| Field | Meaning |
| - | - |
| `moved_count` | Source assignments moved to the target for contacts that did not already have it. |
| `deduplicated_count` | Source assignments removed because the contact already had the target. |
| `repointed_pin_count` | Existing mappings from numeric legacy tag IDs redirected to the target. This does not count RSVP tag changes. |

Moved and duplicate counts include assignments to deleted team contacts too,
so they can exceed the change in the target's `contact_count`.

> [!WARNING]
>
> Merging is not idempotent and has no `Idempotency-Key` support. A retry after a
> successful merge returns `404 tag_not_found` because the source is gone. If a
> response is lost, inspect the source and target before deciding what to do next.

### References that block a merge

The API refuses to merge a source tag that is still used by current audience,
automation, access, or event settings. It does not rewrite those rules for you.
A `409 tag_in_use` error includes `errors[0].meta.references`, a preview of at
most **20** references, each with `category`, `id`, and `name`.
`errors[0].meta.counts` gives the full count for every category, including those
not shown in the preview:

| Count key / category | References counted |
| - | - |
| `segments` | Active, non-deleted segments that reference the source tag. |
| `automations` | Draft definitions referencing the tag on non-deleted automations with status `draft`, `active`, or `paused`. |
| `automation_versions` | Versions referencing the tag that are current on those automations, or have active or waiting enrollments on a non-deleted automation. |
| `automation_triggers` | Enabled trigger entry filters referencing the tag on non-deleted automations with status `draft`, `active`, or `paused`. |
| `access_rules` | Access rules in non-deleted hubs that use an active segment referencing the tag. |
| `hub_events` | Non-deleted, scheduled events in non-deleted hubs whose end time is still in the future and whose RSVP tag is the source. |

Update the blocking settings, then try again. Counts describe references in
each category; one automation can contribute to multiple categories.

### Merge errors

| Status / code | Meaning |
| - | - |
| `403` | The caller is not the team owner, or used an API key. |
| `404 tag_not_found` | Either tag is missing, deleted, or belongs to another team. Also returned when retrying after success. |
| `409 tag_in_use` | Current references block the merge; inspect the error's `meta.references` and `meta.counts`. |
| `409 merge_too_large` | The source has more than **10,000** assignments, including assignments to deleted team contacts. The error's `meta.assignment_count` gives the count and `meta.limit` is `10000`. |
| `409 merge_busy` | Another operation prevented the merge from completing. No changes were made, so retrying is safe. The response includes `Retry-After: 1`; wait at least one second before retrying. |
| `422 self_merge` | Source and target are the same tag. `source.pointer` is `/data/attributes/target_tag_id`. |
| `422` | The request body does not match the required shape. |
| `429` | The merge rate limit of 60 requests per hour was reached; use `Retry-After`. |

## Find hubs using an attribute

Check which hubs have settings for a contact attribute:

```text theme={null}
GET /api/v1/teams/{team_id}/contact-attributes/{definition_id}/hubs?page[size]=20
```

Each item is a `contact_attribute_hub_usages` resource. Its `id` identifies the
hub's attribute configuration, and its `attributes` contain:

| Attribute | Meaning |
| - | - |
| `hub_id` / `hub_name` | Hub ID and title. |
| `definition_id` | Contact attribute definition being configured. |
| `is_in_profile` | Whether the field is configured to appear in profiles. |
| `is_in_onboarding` | Whether the field is configured to appear during onboarding. |
| `is_required` | Whether the hub marks the field as required during onboarding. |
| `is_read_only` | Whether the hub marks the field as read-only. |
| `is_searchable` | Whether the hub marks the field as searchable in its member directory. |
| `position` | Configured display position within the hub. |

Only non-deleted hubs in the team are included. Explicit configurations with
all flags set to `false` are still returned; a hub with no configuration is
absent. No contact values are returned.

> [!NOTE]
>
> Results use configuration ID ascending. Preserve this page order when loading
> more results: `position` describes the field's display position within a hub,
> and does not sort this list of hubs.

## Contact email engagement

Review tracked opens and clicks for emails sent to one team contact:

```text theme={null}
GET /api/v1/teams/{team_id}/contacts/{team_contact_id}/email-engagement?page[size]=20
```

Each item is a `contact_email_engagements` resource whose `id` identifies an
email send. Results are newest first by `queued_at`, then by send ID descending
when times match. Each item's `attributes` contain:

| Attribute | Meaning |
| - | - |
| `hub_id` | Hub associated with the send. Sends from deleted hubs remain in the list. |
| `subject` / `template_ref` | Subject and template reference; redacted to `null` for an erased contact. |
| `send_status` | Current send status: `queued`, `sending`, `suppressed`, `sent`, `delivered`, `bounced`, `complained`, or `failed`. |
| `queued_at` / `sent_at` | Time the send was requested and time it was sent. `sent_at` can be `null`. |
| `opened` / `clicked` | Whether at least one corresponding tracked event exists. |
| `open_count` / `click_count` | Counts of tracked open and click events for this send, including repeated events. These are not counts of unique people. |
| `first_opened_at` / `last_opened_at` | Earliest and latest tracked open times, or `null` if none exist. |
| `first_clicked_at` / `last_clicked_at` | Earliest and latest tracked click times, or `null` if none exist. |

Authentication emails, including those containing sign-in or recovery links,
are omitted. The response does not include email addresses, message bodies,
links, or raw delivery feedback.

> [!WARNING]
>
> Opens include automated and prefetch opens; the API does not filter them out.
> Zero opens does not prove an email was unread. Tracking may be disabled, a
> custom SMTP connection may not provide feedback, or feedback may be missing.
> Treat these fields as observed events, not proof that a person read the email.

## Read definitions from the hub editor

The hub inline editor's onboarding question picker needs the team's attribute definitions, but the editor signs in as a hub contact, not a team user. Use the hub-scoped read:

```text theme={null}
GET /api/v1/teams/{team_id}/hubs/{hub_id}/contact-attribute-definitions?page[size]=20
```

It returns the same document as `GET /api/v1/teams/{team_id}/contact-attributes`, with the same ordering and `page[size]` limits (`1` to `100`, default `20`). The team-level route stays limited to team owners and admins. Callers allowed on the hub-scoped route:

* a team owner or admin;
* the owner or admin contact of the hub named in the path;
* platform and API-key callers, with the same access as the team route.

The hub in the path scopes the grant. A contact can only read definitions for a team in which it edits that hub. A hub outside the team, or a contact with no standing on the hub, gets `404`. A member or moderator gets `403`.

Definitions are paged by `(position, id)`. Pass `meta.page.next_cursor` back as `page[after]` and treat it as opaque. Cursors issued by this route start with `cad1:`, but you should not build or parse them. A cursor that is malformed or out of range returns `422 invalid_cursor` with `source.parameter` set to `page[after]`.

> [!NOTE]
>
> Reordering definitions between two page requests can repeat or skip a row. A walk with no reorder in between is exact. Restart from the first page if you need a clean list after a reorder.

## Pagination and read errors

The tag-assignment, attribute-hub, and email-engagement lists accept
`page[size]` from `1` to `100` (default `20`) and an opaque `page[after]` cursor.
Responses include `meta.page.size`, `meta.page.has_more`,
`meta.page.next_cursor`, and `links.self` / `links.next`. Follow `links.next`,
or pass `meta.page.next_cursor` as the next request's `page[after]`.
At the end, `has_more` is `false` and both next values are `null`.

Cursors belong to the team and the specific tag, definition, or contact in the
request. Do not reuse them for another list. If the item the cursor points to
is removed from that list, restart without a cursor.

> [!NOTE]
>
> During alpha, pagination differs between these lists and `GET /tags`, which
> uses a tag ID cursor and `meta.has_more`. Returned pagination links currently
> omit the `/api/v1` prefix (`/api/teams/...`). Either follow them as-is or pass
> `meta.page.next_cursor` as `page[after]` on the `/api/v1/teams/...` path.

| Status / code | Meaning |
| - | - |
| `404 tag_not_found` | The requested tag is missing, deleted, or belongs to another team. |
| `404 definition_not_found` | The attribute definition is missing, deleted, or belongs to another team. |
| `404 contact_not_found` | The team contact is missing, deleted, or belongs to another team. |
| `422 invalid_cursor` | A cursor for one of the three lists above is invalid, has the wrong scope, or no longer points to an included item. `source.parameter` is `page[after]`. |
| `422` | `page[size]` is outside `1`–`100` or is not an integer. |
| `429` | A read route's limit of 300 requests per hour was reached; use `Retry-After`. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.