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

# Changelog

> Track changes, new features, and improvements to the Cometly API.

## v1.3.0

### September 9, 2026

**Company external ID on Create Event** — [`POST /events/track`](/api-reference/endpoint/create-event) now accepts two new optional fields: `company_external_id` and `company_name`.

* `company_external_id` groups this contact into a company using your own account or company identifier. It's trimmed, case-preserved (never lowercased), and stored byte-exact — 1 to 190 characters after trimming. Floats, arrays, and booleans are rejected with a `422`.
* `company_name` is a display name for the company identified by `company_external_id` — 1 to 255 characters after trimming, same type and validation rules. Last write wins, and it's never used for matching: two companies with the same name stay separate. It's ignored when `company_external_id` isn't also sent.
* Blank strings for either field are treated as absent (`null`).
* This is unrelated to `tracking_id`: `tracking_id` merges events into a single contact and never groups companies, while `company_external_id` groups contacts into a company and never merges contacts or takes part in contact matching. Never send a shared account ID as `tracking_id`.
* `company_external_id` is only honored when the Space's company identity mode is set to **External ID** in Space Settings. In the default **Auto** mode (companies grouped by business-email domain), the value is accepted and stored but ignored for grouping.

**Company `external_id` on the Companies API** — [Get Company](/api-reference/endpoint/get-company), [List Companies](/api-reference/endpoint/list-companies), and [Update Company](/api-reference/endpoint/update-company) now expose `external_id`, the customer-supplied external ID counterpart to `domain`.

* `external_id` is an identifier, not just a label: it decides which company future events carrying that `company_external_id` are matched to, the same way `domain` decides matching by email domain.
* Update Company now accepts `external_id` (`string | integer | null`) with the same semantics as `domain`: trimmed, but case is **preserved** (byte-exact — `ACCT_1` and `acct_1` are different ids), 1 to 190 characters, integers stored as their decimal string, and a blank string treated as `null`.
* Setting an `external_id` that already belongs to another company in the Space is rejected with `409` — `{ message, field, conflicting_company_id }` — and nothing is written. `field` can now be `domain` or `external_id`.
* Sending `external_id: null` releases the company's claim on the identifier; the company keeps its `id`, history, and contacts.
* The "at least one field" validation message on Update Company is now "At least one of domain, external\_id or name must be provided."
* `external_id` is only honored by event ingestion when the Space's company identity mode is **External ID**. In the default **Auto** mode, the value is stored and returned through this API but ignored for grouping.

**New endpoints: Attach / Detach Contact Company** — [`PUT /contacts/{id}/company`](/api-reference/endpoint/attach-contact-company) and [`DELETE /contacts/{id}/company`](/api-reference/endpoint/detach-contact-company) let you move a contact onto a different company, or off its company entirely, without waiting for another event.

* A contact belongs to at most one company per Space — attaching moves it off any company it currently belongs to.
* The contact's conversions, touchpoints, and visit metadata are re-stamped with the new (or `null`) company id in the same transaction, so company-level reports and the [Companies dataset](/data-warehouse/datasets/companies) in the data warehouse follow immediately.
* Both calls are idempotent: repeating an attach with the contact's current company id, or detaching an already-company-less contact, re-stamps rows (or no-ops) with a `200` rather than an error.
* Rate limited at **60 requests per minute** per Space, the same bucket as [Update Contact](/api-reference/endpoint/update-contact).
* What happens next depends on the Space's company identity mode: in **Auto** mode, the tracker only assigns a company to a contact that currently has none, so a manual attach sticks and a manual detach is undone by the contact's next business-email event; in **External ID** mode, the next event carrying a different `company_external_id` re-points the contact again either way.
* There is no MCP tool for these endpoints yet.

**Webhook mapping destinations** — `Company External ID` and `Company Name` are now available as mapping destination fields on every webhook integration, alongside your existing field mappings. They map to the same `company_external_id` and `company_name` keys as [Create Event](/api-reference/endpoint/create-event).

**Company Tracking settings** — a new **Space Settings → Additional Setup → Company Tracking** page holds the company identity mode (**Auto**, the default, groups companies by business-email domain; **External ID** groups them by `company_external_id` alone) alongside the ad-platform conversion identity mode. `company_external_id` sent on events is only honored in External ID mode.

**New guide: Account-level attribution for B2B** — [Account-level attribution for B2B (company external ID)](/guides/b2b-account-attribution) walks through using `company_external_id` to attribute a conversion by one teammate to an ad another teammate clicked, including webhook/Stripe metadata mapping and the ad-platform conversion identity setting.

***

## v1.2.0

### September 3, 2026

**Email lookup for List Contacts** — [`GET /contacts`](/api-reference/endpoint/list-contacts) now accepts an `email` query parameter to look contacts up by exact address instead of, or in addition to, a creation-date range. Accepts a single address, a comma-separated list, or a repeated `email[]` parameter — up to **100** addresses per request.

* Matching is exact (not partial or fuzzy) and case-insensitive, with surrounding whitespace trimmed. It resolves against the contact's primary email, all secondary emails, and emails captured on profiles that were later merged into the contact, so an address that only ever appeared on a merged-away profile still finds the surviving contact.
* `start_date` / `end_date` are now optional when `email` is supplied, but remain required as a pair otherwise — sending only one is a `422` regardless of `email`.
* `email` and a date range can be combined; both conditions AND together.
* No match returns a normal `200` with an empty `data` array, never a `404`, and several distinct contacts can legitimately share an address — all of them are returned.
* The rate limit is unchanged at **15 requests per minute** per Space, which is why the batch form exists: batch up to 100 addresses into a single call rather than one call per address for bulk reconciliation.
* The [`list_contacts`](/mcp/tools/list-contacts) MCP tool supports the same `email` parameter (as an array only — no comma-separated string form).

**New endpoint: Get Contact by Email** — [`GET /contacts/by-email/{email}`](/api-reference/endpoint/get-contact-by-email) and `GET /contacts/by-email?email=` look up a single contact by address, for when you expect exactly one match. The response is identical to [Get Contact](/api-reference/endpoint/get-contact), including all of its optional query parameters (`include_comet_tokens`, `include_events`, `hide_direct_touchpoints`, `use_custom_field_labels`, `include_browsing_session_data`).

* Resolution uses the same rules as the `email` filter above: exact, case-insensitive, whitespace-trimmed, spanning the contact's primary email, secondary emails, and emails from merged-away profiles.
* Returns `404` when no contact matches, and `409` with `contact_ids` when more than one contact matches — mirroring the `409` on [Delete Contact PII](/api-reference/endpoint/delete-contact-pii). Use the List Contacts `email` filter to see all matches, or request one of the returned `contact_ids` via `GET /contacts/{id}`.
* Rate limited at **30 requests per minute** per Space — the same bucket as [Get Contact](/api-reference/endpoint/get-contact), not the 15/minute limit on List Contacts.

***

## MCP v2.3.0

### September 2, 2026

**Personal access tokens (PATs) are now available for server-side and headless MCP agents.** If you're running a CI job, backend service, or any script without a browser, you no longer need to work around the OAuth flow. Generate a token directly from **Settings → Integrations → MCP** under **MCP Access Tokens**, name it, and send it as `Authorization: Bearer` like any other MCP token.

Key details:

* A personal access token behaves exactly like an OAuth-issued token: tied to your user account, working for a single Space, with your Space access re-checked on every request.
* Tokens don't expire. Revoke them when the agent is decommissioned.
* You can generate tokens until you hold up to **25 MCP tokens for a Space**; tokens issued to OAuth clients count toward that number.
* All MCP tokens, OAuth- and PAT-issued alike, are listed and revocable from the same **Settings → Integrations → MCP** page.

See [Authentication](./mcp/authentication#server-side-agents-pat) for the full flow and a `.mcp.json` config example.

***

## v1.1.1

### September 2, 2026

**Company identity rebuild** — a company's stable identity is now its numeric `id`, matched via lookup tables keyed on an email `domain` and/or a customer-supplied external ID (each unique per Space). This changes `domain` semantics across the Companies endpoints:

* **`domain` is now nullable** on [Get Company](/api-reference/endpoint/get-company) and [List Companies](/api-reference/endpoint/list-companies). It's `null` for a company identified only by an external ID, with no associated email domain.
* **[Update Company](/api-reference/endpoint/update-company)**: `domain` is an identifier, not just a label — changing it moves the company's claim on that domain. Values are trimmed and lowercased before storing (a blank string is treated as `null`), matching ingestion. Sending `domain: null` releases the company's claim on its domain: the company keeps its `id`, history, and contacts, but future events from that domain will create or match a different company. If the new domain already belongs to another company in the Space, the request is now rejected with **`409 Conflict`** — `{ message, field: "domain", conflicting_company_id }` — and nothing is changed.
* **`name: null` is now rejected.** The tracker's `name` column is required, so sending `name: null` on Update Company now returns `422` ("The name must be a string and cannot be null.") instead of clearing the value. Omit the key to leave `name` untouched.
* **Companies dataset** (Data Warehouse): `domain` is no longer a locked column — it's optional and nullable, matching the API. A new optional `external_id` column is available (the customer's own company identifier). `id` and `created_at` remain locked.

`external_id` is not yet exposed on the Companies API responses — that's tracked separately.

***

## v1.1.0

### August 7, 2026

**Delete Contact PII endpoint** — Added [`DELETE /contacts/{id}/pii`](/api-reference/endpoint/delete-contact-pii) and `DELETE /contacts/pii?email={email}` for permanently scrubbing a contact's personally identifiable information. Exactly one of `id` or `email` must be provided. The request validates and resolves the target contact synchronously, then queues the scrub and returns `202 Accepted` with `{ contact_id, status: "queued", message }` — call [Get Contact](/api-reference/endpoint/get-contact) afterward to confirm the fields have cleared.

* **Deleted**: all emails, phones, names, locations, devices, IPs, tracking IDs, and comet tokens for the contact (including data inherited from merged profiles), all raw browsing-session hits, and the contact's own `name`, `email`, `phone`, `location`, `city`, `state`, `country`, `device_type`, `os`, `browser`, `language`, and all 30 custom fields.
* **Kept**: the contact record (`id`, `created_at`), its events and journey, and its merge history. Event metadata (webhook payloads, URL query strings) can still contain PII — delete individual events via [Delete Event](/api-reference/endpoint/delete-event) if needed.
* Returns `404` if the `id` or `email` doesn't resolve to a contact in your space, and `409` with `contact_ids` if `email` matches more than one distinct contact — nothing is deleted in that case. Unlike [Delete Event](/api-reference/endpoint/delete-event), this endpoint is **not** idempotent on missing targets.

***

## MCP v2.2.0

### June 18, 2026

**OAuth Client ID Metadata Documents (CIMD) are now supported and are the preferred registration path for new custom MCP clients.** Instead of calling the registration endpoint, you host a JSON metadata document at a stable `https://` URL and use that URL as your `client_id` — no Dynamic Client Registration call required. Cometly fetches, validates, and caches the document on the first `/authorize` request.

Key details:

* The document must include `client_id` (equal to its own URL), `client_name`, `redirect_uris`, and `token_endpoint_auth_method: "none"`.
* Redirect URIs must be loopback or same-origin https (same scheme, host, and port as the `client_id` URL). Cross-origin https and custom URI schemes are not accepted for CIMD clients — use DCR for custom schemes.
* The document is cached up to 24 hours (respecting `Cache-Control: max-age`; default window 15 minutes).
* Support is advertised as `client_id_metadata_document_supported: true` in the authorization server metadata. DCR remains available as a fallback.

See [Authentication](./mcp/authentication#client-id-metadata-document-cimd) for the full specification and a document example.

***

## MCP v2.1.2

### June 10, 2026

**Major hosted MCP clients now complete OAuth Dynamic Client Registration out of the box, with no operator configuration required.** Claude (claude.ai), ChatGPT, Cursor, VS Code for the Web, Grok, Gemini, and Perplexity are all accepted by default. See [Authentication](/mcp/authentication#default-allowed-https-hosts) for the full host list.

***

## MCP v2.1.1

### June 3, 2026

**Dynamic Client Registration now accepts a partial `redirect_uris` array.** Previously, if any single URI in the `redirect_uris` array failed the redirect URI policy, the entire registration was rejected with `400 invalid_redirect_uri`. Now the endpoint registers only the valid subset and drops the rest — a `201` response's `redirect_uris` field reflects the accepted URIs, which may be fewer than you submitted. The registration is only rejected outright when **none** of the supplied URIs are valid. The per-URI validation rules (loopback, allowlisted https host, approved custom scheme, reverse-DNS scheme) are unchanged. See [Authentication](/mcp/authentication) for the full policy table.

***

## v1.0.8

### May 26, 2026

Bug fixes and behavioral improvements for `custom_field_filters` on [List Contacts](/api-reference/endpoint/list-contacts) and [List Events](/api-reference/endpoint/list-events):

* **Age operators on date fields now work.** `age_greater_than`, `age_equal_to`, and `age_less_than` were documented but incorrectly rejected with a 422 when passed a whole-number day count. The validator now accepts numeric values for these operators on date fields (`profile_field_26`–`profile_field_30`).
* **Date filter comparisons are timezone-aware.** `equal_to`, `greater_than`, `less_than`, and `between` on date fields now evaluate against your space's configured timezone. `equal_to` expands to a full midnight-to-midnight range for the given day; `greater_than` compares against end-of-day. Previously these operators compared raw strings, which would miss column values with a stored time component.
* **Numeric `any` / `unknown` semantics.** For numeric fields, `any` matches contacts where the value is greater than `0`; `unknown` matches contacts where the value is `0` or unset.
* **`not_equal_to` and `not_contains` no longer include unset values.** Previously these operators could return contacts where the field was NULL or empty. If you need those contacts, add a separate `unknown` condition ANDed alongside.
* **Text filter literals.** The `_` and `\` characters in `contains`, `starts_with`, `ends_with`, and `contains_word` filter values are now treated as literal characters instead of LIKE wildcards.

***

## MCP v2.1.0

### May 21, 2026

**Removed** the four export tools — `request_event_export`, `request_contact_export`, `request_company_export`, and `get_export_status`. Bulk extraction is better handled by the [Public API V1 export endpoints](/api-reference/endpoint/create-event-export) (programmatic) or the in-app CSV export buttons (interactive). Routing the rows through an MCP turn just consumed tool slots and added a polling round-trip without giving the agent any analytical leverage. Use [`query_events_aggregated`](/mcp/tools/query-events-aggregated), [`query_contacts_aggregated`](/mcp/tools/query-contacts-aggregated), or the various `query_ad_metrics_*` tools when the agent needs to *reason* about large datasets — not export them.

The MCP catalog is now **50 external tools across 14 toolsets** (down from 54 / 15).

***

## MCP v2.0.0

### May 15, 2026

Major expansion of the Cometly MCP server — from **7 read-only tools to 54 external tools** across 15 toolsets, plus the first MCP **Resources** and **Prompts**. Backwards compatible: every v1 tool kept its original input/output shape, with `format: "csv" | "json" | "yaml"` added as an opt-in.

**New toolsets** (49 new tools):

* **Workspace orientation** — [`get_workspace_context`](/mcp/tools/get-workspace-context), [`list_custom_events`](/mcp/tools/list-custom-events), [`list_my_spaces`](/mcp/tools/list-my-spaces), [`list_metric_catalog`](/mcp/tools/list-metric-catalog), [`list_filterable_fields`](/mcp/tools/list-filterable-fields), [`list_user_defined_metrics`](/mcp/tools/list-user-defined-metrics), [`resolve_date_range`](/mcp/tools/resolve-date-range).
* **Search** — [`search_workspace`](/mcp/tools/search-workspace).
* **Analytics depth** — [`query_ad_metrics_compare`](/mcp/tools/query-ad-metrics-compare), [`query_attribution_models`](/mcp/tools/query-attribution-models), [`query_metric_breakdown`](/mcp/tools/query-metric-breakdown).
* **Event aggregations** — [`query_events_aggregated`](/mcp/tools/query-events-aggregated), [`list_event_types`](/mcp/tools/list-event-types).
* **Journeys** — [`get_contact_journey`](/mcp/tools/get-contact-journey), [`query_contacts_aggregated`](/mcp/tools/query-contacts-aggregated), [`get_company_journey`](/mcp/tools/get-company-journey).
* **Ads Manager hierarchy** — [`list_ad_accounts`](/mcp/tools/list-ad-accounts), [`list_campaigns`](/mcp/tools/list-campaigns), [`list_adsets`](/mcp/tools/list-adsets), [`list_ads`](/mcp/tools/list-ads), [`get_ad_entity`](/mcp/tools/get-ad-entity).
* **Reports & dashboards** — [`list_dashboards`](/mcp/tools/list-dashboards), [`get_dashboard`](/mcp/tools/get-dashboard), [`list_reports`](/mcp/tools/list-reports), [`get_report_definition`](/mcp/tools/get-report-definition), [`run_saved_report`](/mcp/tools/run-saved-report), [`analyze_report`](/mcp/tools/analyze-report), [`list_subscriptions`](/mcp/tools/list-subscriptions), [`get_subscription_last_run`](/mcp/tools/get-subscription-last-run).
* **Segments** — [`list_segments`](/mcp/tools/list-segments), [`get_segment`](/mcp/tools/get-segment), [`query_segment_membership`](/mcp/tools/query-segment-membership).
* **Integrations** — [`list_integrations`](/mcp/tools/list-integrations), [`get_pixel_status`](/mcp/tools/get-pixel-status), [`list_event_mappings`](/mcp/tools/list-event-mappings).
* **Health & diagnostics** — [`get_space_health`](/mcp/tools/get-space-health), [`debug_data_flow`](/mcp/tools/debug-data-flow), plus the existing `inspect_site` / `verify_pixel_events`.
* **Webhook debugging** — [`list_recent_webhooks`](/mcp/tools/list-recent-webhooks), [`get_webhook_event`](/mcp/tools/get-webhook-event) (payloads PII-redacted; prompt-injection patterns flagged in a warning footer).
* **Exports** — `request_event_export`, `request_contact_export`, `request_company_export`, `get_export_status`. (Removed in v2.1.0.)
* **AI helpers** — [`search_cometly_docs`](/mcp/tools/search-cometly-docs), [`get_chart_spec_template`](/mcp/tools/get-chart-spec-template).

**Resources** (read-only, subscribable):

* `cometly://workspace/context`
* `cometly://catalog/metrics`
* `cometly://catalog/attribution-models`
* `cometly://catalog/dimensions`
* `cometly://catalog/custom-events`

**Prompts** (slash-command templates):

* `analyze_performance`
* `compare_attribution_models`
* `weekly_review`
* `find_underperformers`

**Cross-cutting improvements:**

* **Custom event label resolution** — analytics tools accept either `custom_event_2` or the configured label ("Deal Created") in their inputs. Output rewrites slot ids to labels with a `# label_map:` footer for round-tripping.
* **Empty-state hints** — analytics tools annotate suspicious all-zero results with hints like "No facebook\_ads account is connected to this space" or "Last google\_ads sync was 3 days ago".
* **Token-budget pagination** — responses cap around \~6,000 tokens with a `truncated_at_token_budget: true` footer + opaque `next_cursor`.
* **CSV default for new tools** — \~50% fewer tokens than JSON for tabular results. Legacy v1 tools keep JSON as default.
* **DST caveat** — `query_ad_metrics` and `resolve_date_range` surface a DST advisory when the requested window crosses a transition.
* **Honest forecasting language** — softened "anomaly detection" / "forecast" claims in tool descriptions to "directional reads" — we don't run statistical models, the agent does threshold math.
* **`external: false` filtering** — tools that only make sense in-app (chat history, file attachments) are hidden from external MCP clients via a server-level `shouldRegister` filter.

***

## v1.0.7

### May 14, 2026

* **Update Contact endpoint** — Added [PUT /contacts/{id}](/api-reference/endpoint/update-contact) for updating an existing contact's `emails`, `phones`, `names`, `locations`, and `custom_fields`. Uses **sparse PUT** semantics: any field you include replaces the existing collection in full; any field you omit is left untouched. Empty arrays (`[]`) wipe a collection; providing `custom_fields` clears every key not in the object.
* **Update Company endpoint** — Added [PUT /companies/{id}](/api-reference/endpoint/update-company) for updating a company's `domain` and `name`. Uses sparse PUT semantics — only the fields you include are modified.
* **`name` field on Get Company** — The [Get Company](/api-reference/endpoint/get-company) response now includes `name` alongside `id` and `domain`. This is an additive change; existing fields are unchanged.
* **`custom_field_filters` parameter on List Contacts** — Added `custom_field_filters` to the [List Contacts](/api-reference/endpoint/list-contacts) endpoint. Accepts an array of AND-joined conditions (max 10) for filtering contacts by their custom profile field values. Each condition takes `{ field, operator, value }` (or `{ field, operator, start, end }` for the `between` operator). The `field` must reference a `profile_field_N` slot configured with a label in your space; operator vocabulary depends on the field's type (text / number / date).
* **`custom_field_filters` parameter on List Events** — Same parameter added to the [List Events](/api-reference/endpoint/list-events) endpoint, narrowing results to events whose owning contact matches the conditions. Same shape and operator vocabulary as List Contacts.

***

## v1.0.6

### April 20, 2026

* **`include_browsing_session_data` parameter on Get Contact** — Added `include_browsing_session_data` to the [Get Contact](/api-reference/endpoint/get-contact) endpoint. When set to `1`, the response includes a `browsing_session_hits` array with up to the last 1000 raw browsing hits (page views) for the contact, ordered by most recent first. Each hit includes `event_url` (full URL), `referrer_host`, `source`, `country`, `device_type`, `browser`, `os`, `event_name`, and `event_time_utc`.

***

## v1.0.5

### April 7, 2026

* **`include_events` parameter on Get Contact** — Added `include_events` to the [Get Contact](/api-reference/endpoint/get-contact) endpoint. When set to `1`, the response includes an `events` array containing the contact's full journey — all conversions and touchpoints sorted by most recent first. Conversion events include `event_name`, `amount`, `event_url`, `order_id`, `order_name`, `channel` (browser/server), and `integration`. Touchpoint events include `source`, `touchpoint_url`, `referrer_url`, and the full ad hierarchy (`ad_name`, `adset_name`, `campaign_name`, `account_name`).
* **`hide_direct_touchpoints` parameter on Get Contact** — Added `hide_direct_touchpoints` (default `1`) to the [Get Contact](/api-reference/endpoint/get-contact) endpoint. When `include_events=1`, direct touchpoints are excluded by default. Set to `0` to include them.

***

## v1.0.4

### March 19, 2026

* **`use_custom_field_labels` parameter** — Added `use_custom_field_labels` to the [List Contacts](/api-reference/endpoint/list-contacts) and [Get Contact](/api-reference/endpoint/get-contact) endpoints. When set to `1`, custom field keys use user-defined labels (e.g. `"Customer Age"`) instead of raw column names (e.g. `"profile_field_1"`). Fields without a configured label retain their raw column name.

***

## v1.0.3

### February 27, 2026

* **Ad hierarchy fields on List Events** — Added `ad_name`, `adset_id`, `adset_name`, `campaign_id`, `campaign_name`, `account_id`, and `account_name` as requestable fields on the [List Events](/api-reference/endpoint/list-events) endpoint. These fields require `sources` to be provided and return the ad, ad set, campaign, and account data from the attributed touchpoint.

***

## v1.0.2

### February 19, 2026

* **`include_all_emails` parameter** — Added `include_all_emails` to the [List Contacts](/api-reference/endpoint/list-contacts) and [Create Contact Export](/api-reference/endpoint/create-contact-export) endpoints. When set to `1`, returns all email addresses associated with a contact, including emails from merged profiles.

***

## v1.0.1

### February 17, 2026

* **Custom fields on Create Event** — The [Create Event](/api-reference/endpoint/create-event) endpoint now accepts custom profile fields (`profile_field_1` through `profile_field_30`) as root-level properties. Fields 1-15 accept text, 16-25 accept numbers (string or number), 26-30 accept dates.
* **`event_url` field** — Added `event_url` as a requestable field on the [List Events](/api-reference/endpoint/list-events) and [Create Event Export](/api-reference/endpoint/create-event-export) endpoints. Returns the full URL where the event occurred.

***

## v1.0.0

### Initial release
