# Attach Contact Company
Source: https://docs.cometly.com/api-reference/endpoint/attach-contact-company
PUT https://app.cometly.com/public-api/v1/contacts/{id}/company
Attach a contact to a company, moving it off any company it currently belongs to
## Overview
This endpoint attaches a contact to a company. A contact belongs to at most **one company per Space**, so attaching it to a new company moves it off any company it currently belongs to.
In the same transaction, the contact's conversions, touchpoints, and visit metadata are re-stamped with the new company id, so company-level reports and the [Companies dataset](/data-warehouse/datasets/companies) `company_id` columns in the data warehouse follow immediately — nothing is left pointing at the old company.
This call is **idempotent**: repeating it with the contact's current company id re-stamps the same rows again, which is useful for repairing rows that drifted out of sync rather than a no-op.
## Path Parameters
The unique identifier of the contact to attach. Merged profile aliases are automatically resolved to the current canonical contact — the response's `id` is the surviving contact.
## Request Body
The unique identifier of the company to attach the contact to. Must belong to a company in the same Space as the contact.
## Response
### Success Response
The unique identifier of the contact (after resolving merged-profile aliases).
The id of the company the contact is now attached to — echoes the `company_id` sent in the request.
### Error Response
Error description explaining what went wrong.
## Example Requests
```bash cURL theme={null}
curl -X PUT "https://app.cometly.com/public-api/v1/contacts/12345/company" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"company_id": 54321
}'
```
```javascript JavaScript theme={null}
const contactId = 12345;
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts/${contactId}/company`, {
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
company_id: 54321
})
});
const data = await response.json();
console.log('Attached company:', data);
```
```php PHP theme={null}
54321,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
contact_id = 12345
url = f'https://app.cometly.com/public-api/v1/contacts/{contact_id}/company'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'company_id': 54321,
}
response = requests.put(url, headers=headers, json=data)
result = response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Contact successfully attached to the company |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Contact not found, or `company_id` does not match a company in this Space |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **60 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details, the same bucket as [Update Contact](/api-reference/endpoint/update-contact).
* **One company per contact**: attaching a contact to a new company removes it from any company it previously belonged to — a contact is never attached to more than one company at a time.
* **Rows are re-stamped, not just the pivot**: the contact's conversions, touchpoints, and visit metadata are updated to the new `company_id` in the same transaction, so company-level reports and the data warehouse [Companies dataset](/data-warehouse/datasets/companies) join correctly right away.
* **Idempotent repair**: calling this again with the contact's current company id is not a no-op — it re-stamps the contact's rows, which repairs any that had drifted out of sync.
* **Merged profiles**: If you pass an ID that was merged into another profile, the attach is applied to the canonical (current) profile, and the response's `id` is that canonical id.
* **`company_id` must be in the same Space**: attaching to a company from a different Space returns `404`.
* **Behavior after a manual attach depends on company identity mode**: in the default **Auto** mode, the tracker only assigns a company to a contact that currently has none, so a manual attach sticks until the contact is manually detached — it is not undone by a later event. In **External ID** mode, the next event carrying a different `company_external_id` re-points the contact again.
# Create Company Export
Source: https://docs.cometly.com/api-reference/endpoint/create-company-export
POST https://app.cometly.com/public-api/v1/companies/exports
Create an asynchronous export job to generate a downloadable file of companies
## Overview
This endpoint creates an asynchronous export job that generates a downloadable file containing company data for a specific date range. The export is processed in the background and returns a presigned download URL when complete.
Use this endpoint when you need to extract large volumes of company data efficiently. For real-time needs, use the [List Companies](/api-reference/endpoint/list-companies) endpoint instead.
**When to use exports**: Exports are ideal for bulk data extraction (10,000+ companies). For smaller datasets, use the paginated list endpoint.
## Request Body
### Required Parameters
Start date and time for exporting companies in `YYYY-MM-DD HH:MM:SS` format. Exports companies created from this timestamp in your space's timezone.
Example: `2024-01-15 00:00:00`
End date and time for exporting companies in `YYYY-MM-DD HH:MM:SS` format. Must be after `start_date`. Exports companies created until this timestamp in your space's timezone.
Example: `2024-01-15 23:59:59`
## Response
### Success Response (202 Accepted)
Unique identifier for the export job. Use this to check status with the [Get Company Export](/api-reference/endpoint/get-company-export) endpoint.
Current status of the export. Will be `queued` immediately after creation.
### Error Response
Error description explaining what went wrong
## Export File Format
Completed exports are delivered as **gzipped NDJSON** (Newline-Delimited JSON) files. Each line contains a complete JSON object representing one company record.
```json theme={null}
{"id":123,"domain":"example.com","created_at":"2024-01-15T10:23:45Z"}
{"id":124,"domain":"acme.co","created_at":"2024-01-15T14:12:33Z"}
```
This format is ideal for streaming and processing large files line-by-line without loading the entire file into memory.
## Example Requests
```bash cURL theme={null}
# Create export for a date range
curl -X POST "https://app.cometly.com/public-api/v1/companies/exports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"start_date": "2024-01-15 00:00:00",
"end_date": "2024-01-15 23:59:59"
}'
```
```javascript JavaScript theme={null}
// Create export for a date range
const response = await fetch('https://app.cometly.com/public-api/v1/companies/exports', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
start_date: '2024-01-15 00:00:00',
end_date: '2024-01-15 23:59:59'
})
});
const data = await response.json();
console.log(`Export created: ${data.export_id}, Status: ${data.status}`);
```
```php PHP theme={null}
'2024-01-15 00:00:00',
'end_date' => '2024-01-15 23:59:59'
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
echo "Export created: {$result['export_id']}, Status: {$result['status']}\n";
?>
```
```python Python theme={null}
import requests
base_url = 'https://app.cometly.com/public-api/v1/companies/exports'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'start_date': '2024-01-15 00:00:00',
'end_date': '2024-01-15 23:59:59'
}
response = requests.post(base_url, headers=headers, json=data)
result = response.json()
print(f"Export created: {result['export_id']}, Status: {result['status']}")
```
## Status Codes
| Status Code | Description |
| - | - |
| 202 | Export job created and queued for processing |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Next Steps
After creating an export, poll the [Get Company Export](/api-reference/endpoint/get-company-export) endpoint to check the status and retrieve the download URL when ready.
## Notes
* **Rate Limit**: This endpoint has a limit of **5 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Processing Time**: Exports typically complete within seconds to a few minutes depending on data volume
* **Date Range**: Specify any date range using `start_date` and `end_date` (interpreted in your space's timezone)
* **Export Capacity**: Can handle 100,000+ companies per export efficiently
* **Status Checking**: Use the [Get Company Export](/api-reference/endpoint/get-company-export) endpoint to check status (30 requests/min limit)
* **File Expiration**: Download URLs expire after 15 minutes for security. Retrieve the file promptly after completion.
* **Security**: Export files use UUID-based filenames with time-limited presigned URLs to prevent unauthorized access
* **Date Filtering**: Companies are filtered by `created_at` timestamp - only companies created within the specified date range are included
# Create Contact
Source: https://docs.cometly.com/api-reference/endpoint/create-contact
POST https://app.cometly.com/public-api/v1/contacts
Create a new contact with arrays of emails, phones, names, and locations
## Overview
This endpoint allows you to create a new contact in Cometly. You can provide multiple emails, phone numbers, names, and locations for a single contact. The first item in each array will be set as the primary value for that field in the contact's profile.
## Request Body
Array of email addresses for this contact. At least one email is required. Each email must be a valid email address.
Example: `["john@example.com", "john.doe@example.com"]`
Array of phone numbers for this contact. Optional.
Example: `["+1234567890", "+0987654321"]`
Array of names for this contact. Optional.
Example: `["John Doe", "Johnny"]`
Array of locations for this contact. Optional.
Example: `["New York, NY", "Los Angeles, CA"]`
Object of custom profile field values keyed by field slug (`profile_field_1` through `profile_field_30`). Optional.
Fields 1-15 accept text, 16-25 accept numbers, 26-30 accept dates.
Example: `{"profile_field_1": "Enterprise", "profile_field_16": "500"}`
## Response
### Success Response
Returns the created contact with all provided data in the ContactDetail format.
The unique identifier of the newly created contact
Array of email addresses that were provided
Array of phone numbers that were provided
Array of names that were provided
Array of locations that were provided
Object containing the custom profile fields that were provided. Only included if custom fields were passed in the request.
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
curl -X POST "https://app.cometly.com/public-api/v1/contacts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"emails": ["john@example.com", "john.doe@example.com"],
"phones": ["+1234567890", "+0987654321"],
"names": ["John Doe", "Johnny"],
"locations": ["New York, NY", "Los Angeles, CA"]
}'
```
```javascript JavaScript theme={null}
const response = await fetch('https://app.cometly.com/public-api/v1/contacts', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
emails: ['john@example.com', 'john.doe@example.com'],
phones: ['+1234567890', '+0987654321'],
names: ['John Doe', 'Johnny'],
locations: ['New York, NY', 'Los Angeles, CA']
})
});
const data = await response.json();
console.log('Created contact ID:', data.id);
```
```php PHP theme={null}
['john@example.com', 'john.doe@example.com'],
'phones' => ['+1234567890', '+0987654321'],
'names' => ['John Doe', 'Johnny'],
'locations' => ['New York, NY', 'Los Angeles, CA']
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
echo 'Created contact ID: ' . $result['id'];
?>
```
```python Python theme={null}
import requests
url = 'https://app.cometly.com/public-api/v1/contacts'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'emails': ['john@example.com', 'john.doe@example.com'],
'phones': ['+1234567890', '+0987654321'],
'names': ['John Doe', 'Johnny'],
'locations': ['New York, NY', 'Los Angeles, CA']
}
response = requests.post(url, headers=headers, json=data)
result = response.json()
print(f"Created contact ID: {result['id']}")
```
## Status Codes
| Status Code | Description |
| - | - |
| 201 | Contact successfully created |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **60 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Required Field**: At least one email address is required. All other fields are optional.
* **Multiple Values**: You can provide multiple emails, phones, names, and locations for a single contact.
* **Primary Data**: The first item in each array becomes the primary value stored in the contact's profile.
* **All Data Stored**: All items from each array are stored in their respective tables (emails, phones, names, locations) and can be retrieved via GET /contacts/.
* **Transaction Safety**: Contact creation is wrapped in a database transaction to ensure data consistency.
* **Custom Fields**: You can optionally pass a `custom_fields` object with keys `profile_field_1` through `profile_field_30`. Fields 1-15 are text (max 10,240 chars), 16-25 are numeric, 26-30 are dates. Invalid keys are rejected with a 422 error.
# Create Contact Export
Source: https://docs.cometly.com/api-reference/endpoint/create-contact-export
POST https://app.cometly.com/public-api/v1/contacts/exports
Create an asynchronous export job to generate a downloadable file of contacts
## Overview
This endpoint creates an asynchronous export job that generates a downloadable file containing contact data for a specific date range. The export is processed in the background and returns a presigned download URL when complete.
Use this endpoint when you need to extract large volumes of contact data efficiently. For real-time needs, use the [List Contacts](/api-reference/endpoint/list-contacts) endpoint instead.
**When to use exports**: Exports are ideal for bulk data extraction (10,000+ contacts). For smaller datasets, use the paginated list endpoint.
## Request Body
### Required Parameters
Start date and time for exporting contacts in `YYYY-MM-DD HH:MM:SS` format. Exports contacts created from this timestamp in your space's timezone.
Example: `2024-01-15 00:00:00`
End date and time for exporting contacts in `YYYY-MM-DD HH:MM:SS` format. Must be after `start_date`. Exports contacts created until this timestamp in your space's timezone.
Example: `2024-01-15 23:59:59`
### Optional Parameters
When set to `1`, includes the last 5 comet tokens for each contact in the export file, ordered by most recent first. Accepts `1` or `0`.
When set to `1`, includes all email addresses for each contact in the export file, including emails from merged profiles. Accepts `1` or `0`.
## Response
### Success Response (202 Accepted)
Unique identifier for the export job. Use this to check status with the [Get Contact Export](/api-reference/endpoint/get-contact-export) endpoint.
Current status of the export. Will be `queued` immediately after creation.
### Error Response
Error description explaining what went wrong
## Export File Format
Completed exports are delivered as **gzipped NDJSON** (Newline-Delimited JSON) files. Each line contains a complete JSON object representing one contact record.
```json theme={null}
{"id":123,"email":"user@example.com","name":"John Doe","phone":"+1234567890","location":"US"}
{"id":124,"email":"jane@example.com","name":"Jane Smith","phone":"+9876543210","location":"CA"}
```
When `include_comet_tokens=1` is specified:
```json theme={null}
{"id":123,"email":"user@example.com","name":"John Doe","phone":"+1234567890","location":"US","comet_tokens":["token1","token2","token3"]}
{"id":124,"email":"jane@example.com","name":"Jane Smith","phone":"+9876543210","location":"CA","comet_tokens":["token4","token5"]}
```
When `include_all_emails=1` is specified:
```json theme={null}
{"id":123,"email":"user@example.com","name":"John Doe","phone":"+1234567890","location":"US","emails":["user@example.com","old@example.com"]}
{"id":124,"email":"jane@example.com","name":"Jane Smith","phone":"+9876543210","location":"CA","emails":["jane@example.com"]}
```
This format is ideal for streaming and processing large files line-by-line without loading the entire file into memory.
## Example Requests
```bash cURL theme={null}
# Create export for a date range
curl -X POST "https://app.cometly.com/public-api/v1/contacts/exports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"start_date": "2024-01-15 00:00:00",
"end_date": "2024-01-15 23:59:59"
}'
# Include comet tokens in export
curl -X POST "https://app.cometly.com/public-api/v1/contacts/exports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"start_date": "2024-01-15 00:00:00",
"end_date": "2024-01-15 23:59:59",
"include_comet_tokens": 1
}'
# Include all emails in export
curl -X POST "https://app.cometly.com/public-api/v1/contacts/exports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"start_date": "2024-01-15 00:00:00",
"end_date": "2024-01-15 23:59:59",
"include_all_emails": 1
}'
```
```javascript JavaScript theme={null}
// Create export for a date range
const response = await fetch('https://app.cometly.com/public-api/v1/contacts/exports', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
start_date: '2024-01-15 00:00:00',
end_date: '2024-01-15 23:59:59'
})
});
const data = await response.json();
console.log(`Export created: ${data.export_id}, Status: ${data.status}`);
// Include comet tokens in export
const responseWithTokens = await fetch('https://app.cometly.com/public-api/v1/contacts/exports', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
start_date: '2024-01-15 00:00:00',
end_date: '2024-01-15 23:59:59',
include_comet_tokens: true
})
});
// Include all emails in export
const responseWithEmails = await fetch('https://app.cometly.com/public-api/v1/contacts/exports', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
start_date: '2024-01-15 00:00:00',
end_date: '2024-01-15 23:59:59',
include_all_emails: true
})
});
const dataWithTokens = await responseWithTokens.json();
console.log(`Export created: ${dataWithTokens.export_id}, Status: ${dataWithTokens.status}`);
const dataWithEmails = await responseWithEmails.json();
console.log(`Export created: ${dataWithEmails.export_id}, Status: ${dataWithEmails.status}`);
```
```php PHP theme={null}
'2024-01-15 00:00:00',
'end_date' => '2024-01-15 23:59:59'
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
echo "Export created: {$result['export_id']}, Status: {$result['status']}\n";
// Include comet tokens in export
$dataWithTokens = json_encode([
'start_date' => '2024-01-15 00:00:00',
'end_date' => '2024-01-15 23:59:59',
'include_comet_tokens' => true
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataWithTokens);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$responseWithTokens = curl_exec($ch);
$resultWithTokens = json_decode($responseWithTokens, true);
curl_close($ch);
echo "Export created: {$resultWithTokens['export_id']}, Status: {$resultWithTokens['status']}\n";
// Include all emails in export
$dataWithEmails = json_encode([
'start_date' => '2024-01-15 00:00:00',
'end_date' => '2024-01-15 23:59:59',
'include_all_emails' => true
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataWithEmails);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$responseWithEmails = curl_exec($ch);
$resultWithEmails = json_decode($responseWithEmails, true);
curl_close($ch);
echo "Export created: {$resultWithEmails['export_id']}, Status: {$resultWithEmails['status']}\n";
?>
```
```python Python theme={null}
import requests
base_url = 'https://app.cometly.com/public-api/v1/contacts/exports'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
# Basic export
data = {
'start_date': '2024-01-15 00:00:00',
'end_date': '2024-01-15 23:59:59'
}
response = requests.post(base_url, headers=headers, json=data)
result = response.json()
print(f"Export created: {result['export_id']}, Status: {result['status']}")
# Include comet tokens in export
data_with_tokens = {
'start_date': '2024-01-15 00:00:00',
'end_date': '2024-01-15 23:59:59',
'include_comet_tokens': True
}
response_with_tokens = requests.post(base_url, headers=headers, json=data_with_tokens)
result_with_tokens = response_with_tokens.json()
print(f"Export created: {result_with_tokens['export_id']}, Status: {result_with_tokens['status']}")
# Include all emails in export
data_with_emails = {
'start_date': '2024-01-15 00:00:00',
'end_date': '2024-01-15 23:59:59',
'include_all_emails': True
}
response_with_emails = requests.post(base_url, headers=headers, json=data_with_emails)
result_with_emails = response_with_emails.json()
print(f"Export created: {result_with_emails['export_id']}, Status: {result_with_emails['status']}")
```
## Status Codes
| Status Code | Description |
| - | - |
| 202 | Export job created and queued for processing |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Next Steps
After creating an export, poll the [Get Contact Export](/api-reference/endpoint/get-contact-export) endpoint to check the status and retrieve the download URL when ready.
## Notes
* **Rate Limit**: This endpoint has a limit of **5 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Processing Time**: Exports typically complete within seconds to a few minutes depending on data volume
* **Date Range**: Specify any date range using `start_date` and `end_date` (interpreted in your space's timezone)
* **Export Capacity**: Can handle 100,000+ contacts per export efficiently
* **Status Checking**: Use the [Get Contact Export](/api-reference/endpoint/get-contact-export) endpoint to check status (30 requests/min limit)
* **File Expiration**: Download URLs expire after 15 minutes for security. Retrieve the file promptly after completion.
* **Security**: Export files use UUID-based filenames with time-limited presigned URLs to prevent unauthorized access
* **Date Filtering**: Contacts are filtered by creation timestamp - only contacts created within the specified date range are included
* **Comet Tokens**: Use the `include_comet_tokens=1` parameter to include the last 5 comet tokens for each contact in the export file. This parameter is optional and defaults to `0`.
* **All Emails**: Use the `include_all_emails=1` parameter to include all email addresses for each contact in the export file, including emails from merged profiles. This parameter is optional and defaults to `0`.
# Create Event
Source: https://docs.cometly.com/api-reference/endpoint/create-event
POST https://app.cometly.com/public-api/v1/events/track
Create a new event to track conversions and user interactions
## Overview
This endpoint allows you to send event data to Cometly for tracking conversions, page views, and other user interactions. Events are processed asynchronously and will be queued for processing.
## Request Body
### Identification Requirements
**At least one of the following identification methods is required:**
1. **Email**: User's email address
2. **Comet Token**: Cometly tracking token
3. **Fingerprint + IP**: Both fingerprint and IP address must be provided together
The name of the event being tracked. Use one of the standard events or a configured custom event.
**Standard Events:**
* `lead_generated`
* `view_content`
* `schedule`
* `purchase`
* `subscribe`
* `add_to_cart`
* `contact`
* `initiate_checkout`
* `add_payment_info`
* `complete_registration`
* `start_trial`
* `sign_up`
* `submit_application`
* `webinar_registration`
**Custom Events:**
Custom events are named `custom_event_1` through `custom_event_50`. View our [help article](https://help.cometly.com/en/articles/8950752-creating-configuring-events) to learn how to configure custom events prior to sending to Cometly.
### Tracking Parameters
User's email address. Can be used as the primary identification method.
Cometly tracking token. Can be used as the primary identification method.
Browser fingerprint for user identification. Must be provided together with `ip`.
User's IP address. Must be provided together with `fingerprint`.
Custom tracking identifier. Events with the same tracking\_id will be attributed to the same contact.
Groups this contact into a company using your own account or company identifier. Nullable; accepts a string or integer, trimmed, 1–190 characters after trimming. The value is case-preserved and stored byte-exact — it is never lowercased. A blank string is treated as absent (`null`). Sending a float, array, or boolean returns a `422`.
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`.
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 still accepted and stored, but ignored for grouping.
Display name for the company identified by `company_external_id`. Nullable; accepts a string or integer, trimmed, 1–255 characters after trimming. A blank string is treated as absent (`null`). Sending a float, array, or boolean returns a `422`.
Last write wins, and the value is never used for matching — two companies with the same name stay separate. Ignored when `company_external_id` is not also provided, since `company_name` alone doesn't identify a company to name.
### User Information
User's full name
User's first name
User's last name
User's phone number
### Session Information
Browser user agent string
Timestamp of when the event occurred. Accepts ISO 8601 format or Unix timestamp in seconds.
The URL where the event occurred
### Ecommerce Data
Transaction amount (for purchase events)
Unique order identifier. **This field is used in our deduplication logic** — if multiple events are sent with the same `order_id`, they may be treated as duplicates. Ensure each distinct order uses a unique value.
Order name or description
Checkout session token
Indicates if this is an upsell transaction
Common identifier linking upsells to original orders
### Attribution Parameters
Traffic source (e.g., "google", "facebook")
Ad network name
Campaign name or identifier
Ad group name or identifier
Specific ad identifier
Keyword that triggered the ad
Traffic type (e.g., "cpc", "organic")
### Custom Fields
Custom profile field values passed as root-level properties. Fields 1-15 accept text, 16-25 accept numbers, 26-30 accept dates.
Example: `"profile_field_1": "Enterprise", "profile_field_16": "500"`
### Advanced Options
Unique key to prevent duplicate event processing. Use this to safely retry requests.
Set to `true` to prevent this event from being sent to Conversions API integrations
## Response
### Success Response
HTTP status code (202)
Success message indicating the event was queued
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
curl -X POST https://app.cometly.com/public-api/v1/events/track \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"event_name": "purchase",
"email": "customer@example.com",
"amount": 99.99,
"order_id": "ORD-12345",
"url": "https://example.com/checkout/success",
"event_time": "2024-01-15T10:30:00Z",
"company_external_id": "acct_8f3k2",
"company_name": "Acme Inc"
}'
```
```javascript JavaScript theme={null}
const response = await fetch('https://app.cometly.com/public-api/v1/events/track', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
event_name: 'purchase',
email: 'customer@example.com',
amount: 99.99,
order_id: 'ORD-12345',
url: 'https://example.com/checkout/success',
event_time: '2024-01-15T10:30:00Z',
company_external_id: 'acct_8f3k2',
company_name: 'Acme Inc'
})
});
const data = await response.json();
```
```php PHP theme={null}
'purchase',
'email' => 'customer@example.com',
'amount' => 99.99,
'order_id' => 'ORD-12345',
'url' => 'https://example.com/checkout/success',
'event_time' => '2024-01-15T10:30:00Z',
'company_external_id' => 'acct_8f3k2',
'company_name' => 'Acme Inc'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
from datetime import datetime
url = 'https://app.cometly.com/public-api/v1/events/track'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'event_name': 'purchase',
'email': 'customer@example.com',
'amount': 99.99,
'order_id': 'ORD-12345',
'url': 'https://example.com/checkout/success',
'event_time': '2024-01-15T10:30:00Z',
'company_external_id': 'acct_8f3k2',
'company_name': 'Acme Inc'
}
response = requests.post(url, headers=headers, json=data)
```
## Status Codes
| Status Code | Description |
| - | - |
| 202 | Event accepted and queued for processing |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 422 | Validation failed or missing required parameters |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **1000 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
## Validation Rules
* At least one identification method must be provided:
* `email`, OR
* `comet_token`, OR
* Both `fingerprint` AND `ip` together
* `event_name` is required
* If `fingerprint` is provided, `ip` must also be provided (and vice versa)
* `event_time` accepts ISO 8601 format or Unix timestamp in seconds
* `amount` must be a number if provided
* `company_external_id`, if provided, must be a string or integer, 1–190 characters after trimming; floats, arrays, and booleans return `422`
* `company_name`, if provided, must be a string or integer, 1–255 characters after trimming; floats, arrays, and booleans return `422`
* Blank strings for `company_external_id` or `company_name` are treated as absent (`null`); `company_name` is ignored when `company_external_id` isn't also provided
## Meta Deduplication
The **`idempotency_key`** in our API is a unique identifier that ensures data consistency and prevents duplication of events. You can extract the **`event_id`** from the Meta browser pixel and use it as the **`idempotency_key`** when interacting with our API. Upon an attributed Meta ad conversion event, we will send this value as `event_id` to prevent duplication, enabling Meta to recognize the `event_id` and deduplicate browser events.
Use the `idempotency_key` parameter to safely retry requests without creating duplicate events. The same idempotency key will prevent the event from being processed multiple times.
# Create Event Export
Source: https://docs.cometly.com/api-reference/endpoint/create-event-export
POST https://app.cometly.com/public-api/v1/events/exports
Create an asynchronous export job to generate a downloadable file of events
## Overview
This endpoint creates an asynchronous export job that generates a downloadable file containing events data. The export is processed in the background and returns a presigned download URL when complete.
Use this endpoint when you need to extract large volumes of event data (10,000+ events) efficiently. For real-time needs, use the [List Events](/api-reference/endpoint/list-events) endpoint instead.
**When to use exports**: See [Exports vs Pagination](/concepts/exports-vs-pagination) to understand when to use exports versus the paginated list endpoint.
## Request Body
### Required Parameters
The date to export events for in `YYYY-MM-DD` format. The date is interpreted in your space's timezone and exports events from 00:00:00 to 23:59:59 for that day.
**Limit**: Maximum 1 day per export.
Example: `2024-01-15`
### Optional Parameters
Determines whether to report metrics by contact or company level.
Options:
* `profile` - Contact-level events (default)
* `company` - Company-level events
Filter results to specific event names. Defaults to all events when omitted.
**Limits:** Minimum 1, maximum 10 event names. Duplicate values are not allowed.
Values:
* `add_payment_info`
* `add_to_cart`
* `complete_registration`
* `contact`
* `initiate_checkout`
* `lead_generated`
* `purchase`
* `schedule`
* `sign_up`
* `start_trial`
* `submit_application`
* `subscribe`
* `upsell_purchase`
* `view_content`
* `webinar_registration`
Custom events: `custom_event_1` through `custom_event_50`.
Filter events by traffic sources. When provided, `attribution_model` and `attribution_window` become required.
**Limits:** Minimum 1, maximum 15 sources. Duplicate values are not allowed.
**Paid Ads:**
`facebook_ads`, `google_ads`, `linkedin_ads`, `tiktok_ads`, `youtube_ads`, `instagram_ads`, `bing_ads`, `yahoo_ads`, `pinterest_ads`, `reddit_ads`, `snapchat_ads`, `telegram_ads`, `twitch_ads`, `x_ads`, `quora_ads`, `duckduckgo_ads`, `whop_ads`
**Organic:**
`facebook_organic`, `google_organic`, `linkedin_organic`, `tiktok_organic`, `youtube_organic`, `instagram_organic`, `bing_organic`, `yahoo_organic`, `pinterest_organic`, `reddit_organic`, `snapchat_organic`, `telegram_organic`, `twitch_organic`, `x_organic`, `quora_organic`, `duckduckgo_organic`, `messenger_organic`, `brave_organic`, `threads_organic`
**AI Platforms:**
`ai_chat_gpt`, `ai_perplexity`, `ai_copilot`, `ai_claude`, `ai_grok`, `ai_gemini`, `ai_deepseek`
**Other:**
`email`, `sms`, `referral`, `direct`, `shop_app`, `substack`, `capterra`, `trustpilot`, `g2`, `yelp`
Attribution model to use for filtering events. Required when `sources` is provided.
Options:
* `first_touch` - First interaction attribution
* `last_touch` - Last interaction attribution
* `linear` - Equal credit across all touchpoints
* `first_platform_touch` - First paid platform touch
* `last_platform_touch` - Last paid platform touch
* `linear_paid` - Linear attribution across paid touchpoints only
* `u_shaped` - 40% first, 40% last, 20% middle touchpoints
* `last_non_direct_touch` - Last touch excluding direct traffic
Attribution lookback window in days. Required when `sources` is provided.
Options: `0` (LTV), `1`, `7`, `14`, `30`, `60`, `90`
Specify which fields to include in the exported file.
**Limits:** Minimum 1, maximum 20 fields. Duplicate values are not allowed.
**Note:** `id` and `event_time_utc` are always included in the export. When `sources` are provided for attribution, `touchpoint_id` is also always included.
Available fields:
* `profile_id` - Contact profile identifier
* `company_id` - Company identifier
* `space_id` - Space identifier
* `event_name` - Event name
* `configured_name` - The configured name of the event. For custom events (`custom_event_1` through `custom_event_50`), this is the user-configured name. For standard events, this equals `event_name`.
* `amount` - Transaction amount (with attribution, this is the partial amount credited to the touchpoint)
* `is_upsell` - Upsell flag
* `ad_id` - Platform-specific ad identifier (requires `sources` to be provided)
* `ad_name` - Ad name from the ad platform (requires `sources` to be provided)
* `adset_id` - Platform-specific ad set identifier (requires `sources` to be provided)
* `adset_name` - Ad set name from the ad platform (requires `sources` to be provided)
* `campaign_id` - Platform-specific campaign identifier (requires `sources` to be provided)
* `campaign_name` - Campaign name from the ad platform (requires `sources` to be provided)
* `account_id` - Platform-specific ad account identifier (requires `sources` to be provided)
* `account_name` - Ad account name from the ad platform (requires `sources` to be provided)
* `order_id` - Order identifier
* `order_name` - Order name
* `event_url` - The full URL where the event occurred
**Auto-included fields (not user-requestable):**
* `id` - Event identifier (always included)
* `event_time_utc` - Event timestamp in UTC (always included)
* `touchpoint_id` - Touchpoint identifier (automatically included when sources are provided)
* `source` - Traffic source (automatically included when any ad hierarchy field is requested: `ad_id`, `ad_name`, `adset_id`, `adset_name`, `campaign_id`, `campaign_name`, `account_id`, `account_name`)
## Response
### Success Response (202 Accepted)
Unique identifier for the export job. Use this to check status with the [Get Event Export](/api-reference/endpoint/get-event-export) endpoint.
Current status of the export. Will be `queued` immediately after creation.
### Error Response
Error description explaining what went wrong
## Export File Format
Completed exports are delivered as **gzipped NDJSON** (Newline-Delimited JSON) files. Each line contains a complete JSON object representing one event record.
```json theme={null}
{"id":123,"event_name":"purchase","amount":99.99,"event_time_utc":"2024-01-15T10:23:45Z"}
{"id":124,"event_name":"sign_up","amount":0,"event_time_utc":"2024-01-15T10:24:12Z"}
{"id":125,"event_name":"purchase","amount":149.50,"event_time_utc":"2024-01-15T10:25:33Z"}
```
This format is ideal for streaming and processing large files line-by-line without loading the entire file into memory.
## Example Requests
```bash cURL theme={null}
# Create export for a single day with minimal parameters
curl -X POST "https://app.cometly.com/public-api/v1/events/exports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"date": "2024-01-15"
}'
# Create export with event filtering and specific fields
curl -X POST "https://app.cometly.com/public-api/v1/events/exports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"date": "2024-01-15",
"query_mode": "profile",
"event_names": ["purchase", "sign_up"],
"fields": ["id", "event_name", "amount", "profile_id", "event_time_utc"]
}'
# Create export with attribution filtering
curl -X POST "https://app.cometly.com/public-api/v1/events/exports" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"date": "2024-01-15",
"sources": ["facebook_ads", "google_ads"],
"attribution_model": "last_touch",
"attribution_window": 30,
"fields": ["id", "event_name", "amount", "ad_id", "event_time_utc"]
}'
```
```javascript JavaScript theme={null}
// Create export for a single day with minimal parameters
const response = await fetch('https://app.cometly.com/public-api/v1/events/exports', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: '2024-01-15'
})
});
const data = await response.json();
console.log(`Export created: ${data.export_id}, Status: ${data.status}`);
// Create export with event filtering and specific fields
const filteredResponse = await fetch('https://app.cometly.com/public-api/v1/events/exports', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: '2024-01-15',
query_mode: 'profile',
event_names: ['purchase', 'sign_up'],
fields: ['id', 'event_name', 'amount', 'profile_id', 'event_time_utc']
})
});
const filteredData = await filteredResponse.json();
// Create export with attribution filtering
const attributedResponse = await fetch('https://app.cometly.com/public-api/v1/events/exports', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
date: '2024-01-15',
sources: ['facebook_ads', 'google_ads'],
attribution_model: 'last_touch',
attribution_window: 30,
fields: ['id', 'event_name', 'amount', 'ad_id', 'event_time_utc']
})
});
const attributedData = await attributedResponse.json();
```
```php PHP theme={null}
'2024-01-15'
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
echo "Export created: {$result['export_id']}, Status: {$result['status']}\n";
// Create export with event filtering and specific fields
$filteredData = json_encode([
'date' => '2024-01-15',
'query_mode' => 'profile',
'event_names' => ['purchase', 'sign_up'],
'fields' => ['id', 'event_name', 'amount', 'profile_id', 'event_time_utc']
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $filteredData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$filteredResponse = curl_exec($ch);
$filteredResult = json_decode($filteredResponse, true);
curl_close($ch);
// Create export with attribution filtering
$attributedData = json_encode([
'date' => '2024-01-15',
'sources' => ['facebook_ads', 'google_ads'],
'attribution_model' => 'last_touch',
'attribution_window' => 30,
'fields' => ['id', 'event_name', 'amount', 'ad_id', 'event_time_utc']
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $attributedData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$attributedResponse = curl_exec($ch);
$attributedResult = json_decode($attributedResponse, true);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
import json
# Create export for a single day with minimal parameters
base_url = 'https://app.cometly.com/public-api/v1/events/exports'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'date': '2024-01-15'
}
response = requests.post(base_url, headers=headers, json=data)
result = response.json()
print(f"Export created: {result['export_id']}, Status: {result['status']}")
# Create export with event filtering and specific fields
filtered_data = {
'date': '2024-01-15',
'query_mode': 'profile',
'event_names': ['purchase', 'sign_up'],
'fields': ['id', 'event_name', 'amount', 'profile_id', 'event_time_utc']
}
filtered_response = requests.post(base_url, headers=headers, json=filtered_data)
filtered_result = filtered_response.json()
# Create export with attribution filtering
attributed_data = {
'date': '2024-01-15',
'sources': ['facebook_ads', 'google_ads'],
'attribution_model': 'last_touch',
'attribution_window': 30,
'fields': ['id', 'event_name', 'amount', 'ad_id', 'event_time_utc']
}
attributed_response = requests.post(base_url, headers=headers, json=attributed_data)
attributed_result = attributed_response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 202 | Export job created and queued for processing |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Next Steps
After creating an export, you need to poll the [Get Event Export](/api-reference/endpoint/get-event-export) endpoint to check the status and retrieve the download URL when ready.
## Notes
* **Rate Limit**: This endpoint has a limit of **5 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Processing Time**: Exports typically complete within seconds to a few minutes depending on data volume
* **Max Date Range**: Each export can only contain **one day** of data (00:00:00 to 23:59:59 in your space's timezone)
* **Export Capacity**: Can handle 100,000+ events per day efficiently
* **Status Checking**: Use the [Get Event Export](/api-reference/endpoint/get-event-export) endpoint to check status (30 requests/min limit)
* **Attribution Breakdown**: With multi-touch attribution models (linear, u\_shaped, linear\_paid), each event may generate multiple rows in the export - one per attributed touchpoint. Each row contains the touchpoint's partial credit (e.g., a \$100 conversion with 4 touchpoints returns 4 rows with \$25 each). Single-touch models (first\_touch, last\_touch) return one row per event.
* When `sources` are not provided, all events in the specified date are exported without attribution filtering
* The ad hierarchy fields (`ad_id`, `ad_name`, `adset_id`, `adset_name`, `campaign_id`, `campaign_name`, `account_id`, `account_name`) require `sources` to be provided. Requesting higher-level hierarchy fields (such as `account_name`) may increase processing time compared to requesting only lower-level fields (such as `ad_name`), so selecting only the fields you need will generally be more efficient.
* By default, only `id` and `event_time_utc` are included in the export (plus `touchpoint_id` when using attribution). Specify additional fields as needed to minimize file size.
* **File Expiration**: Download URLs expire after 15 minutes for security. Retrieve the file promptly after completion.
* **Security**: Export files use UUID-based filenames with time-limited presigned URLs to prevent unauthorized access
# Delete Contact PII
Source: https://docs.cometly.com/api-reference/endpoint/delete-contact-pii
DELETE https://app.cometly.com/public-api/v1/contacts/{id}/pii
Permanently scrub a contact's personally identifiable information, by contact ID or by email
## Overview
This endpoint permanently scrubs the personally identifiable information (PII) stored for a contact, while preserving the contact record itself and its event history for reporting continuity.
You can target the contact either of two ways:
* **By path ID**: `DELETE /contacts/{id}/pii`
* **By email**: `DELETE /contacts/pii?email={email}`
Exactly one of `id` or `email` must be provided — supplying both, or neither, returns a `422`.
This is an **asynchronous** operation. The request validates and resolves the target contact synchronously, then queues the actual scrub and returns immediately with `202 Accepted`. The PII removal itself typically completes within a few minutes; call [Get Contact](/api-reference/endpoint/get-contact) afterward to confirm the fields have been cleared.
### What gets deleted
* All emails, phones, names, locations, devices, IPs, tracking IDs, and comet tokens ever associated with the contact — including data inherited from profiles that were merged into this contact.
* All raw browsing-session hits for the contact (IPs, fingerprints, tokens, and URL query strings). After this completes, `include_browsing_session_data` on [Get Contact](/api-reference/endpoint/get-contact) returns empty, and the contact's page-view hits no longer contribute to analytics counts.
* On the contact record itself: `name`, `email`, `phone`, `location`, `city`, `state`, `country`, `device_type`, `os`, `browser`, `language`, and all 30 custom fields (`profile_field_1`–`profile_field_30`) are set to `null`.
### What is kept
* The contact record itself — its `id` and `created_at` are preserved.
* Its events and journey — touchpoints and conversions, including their metadata, remain intact.
* Its merge history.
Event metadata can still contain PII that arrived inside a webhook payload or a touchpoint's URL query string — deleting a contact's PII does not scrub individual events. To remove PII from a specific event, delete it directly via [Delete Event](/api-reference/endpoint/delete-event).
## Path Parameters
The unique identifier of the contact whose PII should be deleted. Required when not using `email`. Merged profile aliases are automatically resolved to the current canonical contact. Minimum: 1.
## Query Parameters
The email address of the contact whose PII should be deleted. Required when not using `id`. Matches against both the contact's full email history (including emails inherited from merged profiles) and its current primary email. If more than one distinct contact matches the email, the request is rejected with a `409` — delete by `id` instead to disambiguate.
## Response
### Success Response
Returns `202 Accepted`. The scrub is queued, not yet complete.
The canonical contact id (after resolving merged-profile aliases) whose PII was queued for deletion.
Always `queued`.
Human-readable confirmation that the deletion has been queued.
### Error Response
Error description explaining what went wrong.
Only present on a `409` response. The sorted list of canonical contact ids that matched the given `email`. Nothing is deleted when this error is returned — retry the request once per `id` to disambiguate.
## Example Requests
### Delete by Contact ID
```bash cURL theme={null}
curl -X DELETE "https://app.cometly.com/public-api/v1/contacts/12345/pii" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
```
```javascript JavaScript theme={null}
const contactId = 12345;
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts/${contactId}/pii`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
});
const data = await response.json();
console.log('Deletion queued:', data);
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
contact_id = 12345
url = f'https://app.cometly.com/public-api/v1/contacts/{contact_id}/pii'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
response = requests.delete(url, headers=headers)
result = response.json()
```
### Delete by Email
```bash cURL theme={null}
curl -X DELETE "https://app.cometly.com/public-api/v1/contacts/pii?email=john%40example.com" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
```
```javascript JavaScript theme={null}
const email = 'john@example.com';
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts/pii?email=${encodeURIComponent(email)}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
});
const data = await response.json();
console.log('Deletion queued:', data);
```
```php PHP theme={null}
$email]);
$headers = [
'Authorization: Bearer YOUR_API_KEY',
'Accept: application/json'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
email = 'john@example.com'
url = 'https://app.cometly.com/public-api/v1/contacts/pii'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
response = requests.delete(url, headers=headers, params={'email': email})
result = response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 202 | PII deletion successfully queued |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Contact not found (unknown `id` or no contact matches `email`) |
| 409 | Multiple contacts match the given `email` — see `contact_ids` in the response and retry by `id` |
| 422 | Invalid parameters (both `id` and `email` provided, neither provided, or invalid format) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **60 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Not idempotent on missing targets**: unlike [Delete Event](/api-reference/endpoint/delete-event), which returns `200` for an ID that no longer exists, this endpoint returns `404` when the `id` or `email` doesn't resolve to a contact in your space.
* **Asynchronous**: a `202` response means the deletion has been queued, not completed. Poll [Get Contact](/api-reference/endpoint/get-contact) to confirm the PII fields have been cleared.
* **Ambiguous email matches nothing**: if `email` matches more than one distinct contact, the request is rejected with `409` and no data is deleted. Look up the individual contact ids (via `contact_ids` in the response, or [List Contacts](/api-reference/endpoint/list-contacts)) and call this endpoint once per `id`.
* **Merged profiles**: if you pass an `id` that was merged into another profile, the deletion applies to the canonical (current) profile.
This operation is permanent and cannot be undone. The contact record, its events, and its merge history are preserved — only identifying data is scrubbed.
# Delete Event
Source: https://docs.cometly.com/api-reference/endpoint/delete-event
DELETE https://app.cometly.com/public-api/v1/events/{event_id}
Delete a specific event by its ID
## Overview
This endpoint allows you to delete a previously created event from Cometly. Once deleted, the event data will be removed from your tracking and reporting.
This endpoint implements idempotent delete behavior - attempting to delete an event that doesn't exist will still return a success response (200).
## Path Parameters
The unique identifier of the event to delete
## Response
### Success Response
HTTP status code (200)
Success message confirming the event was deleted
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
curl -X DELETE https://app.cometly.com/public-api/v1/events/123456 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json"
```
```javascript JavaScript theme={null}
const eventId = '123456';
const response = await fetch(`https://app.cometly.com/public-api/v1/events/${eventId}`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const data = await response.json();
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
event_id = '123456'
url = f'https://app.cometly.com/public-api/v1/events/{event_id}'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.delete(url, headers=headers)
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Event successfully deleted (or already doesn't exist) |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 422 | Invalid event ID provided |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **60 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
Deleting an event is permanent and cannot be undone. Make sure you have the correct event ID before deleting.
# Detach Contact Company
Source: https://docs.cometly.com/api-reference/endpoint/detach-contact-company
DELETE https://app.cometly.com/public-api/v1/contacts/{id}/company
Detach a contact from its current company
## Overview
This endpoint detaches a contact from its company, leaving it with no company. In the same transaction, the contact's conversions, touchpoints, and visit metadata are re-stamped with a `null` company id, so company-level reports and the [Companies dataset](/data-warehouse/datasets/companies) `company_id` columns in the data warehouse follow immediately.
There is no request body — the contact id in the path is the whole request.
This call is idempotent: calling it on a contact that already has no company returns a normal `200`, not an error.
## Path Parameters
The unique identifier of the contact to detach. Merged profile aliases are automatically resolved to the current canonical contact — the response's `id` is the surviving contact.
## Response
### Success Response
The unique identifier of the contact (after resolving merged-profile aliases).
Always `null` after a successful detach.
### Error Response
Error description explaining what went wrong.
## Example Requests
```bash cURL theme={null}
curl -X DELETE "https://app.cometly.com/public-api/v1/contacts/12345/company" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
```
```javascript JavaScript theme={null}
const contactId = 12345;
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts/${contactId}/company`, {
method: 'DELETE',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
});
const data = await response.json();
console.log('Detached company:', data);
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
contact_id = 12345
url = f'https://app.cometly.com/public-api/v1/contacts/{contact_id}/company'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
response = requests.delete(url, headers=headers)
result = response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Contact successfully detached (also returned as a no-op when the contact already had no company) |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Contact not found |
| 422 | Invalid contact ID provided |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **60 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details, the same bucket as [Update Contact](/api-reference/endpoint/update-contact).
* **No request body**: the contact id in the path is the whole request.
* **Rows are re-stamped, not just the pivot**: the contact's conversions, touchpoints, and visit metadata are updated to a `null` `company_id` in the same transaction, so company-level reports and the data warehouse [Companies dataset](/data-warehouse/datasets/companies) reflect the detach right away.
* **No-op on an already-detached contact**: calling this on a contact with no company still returns `200` with `company_id: null` — it is not an error.
* **Merged profiles**: If you pass an ID that was merged into another profile, the detach is applied to the canonical (current) profile, and the response's `id` is that canonical id.
* **Behavior after a manual detach depends on company identity mode**: in the default **Auto** mode, the tracker assigns a company to any contact that currently has none, so the contact's next event carrying a business-email domain undoes the detach. In **External ID** mode, the next event carrying a `company_external_id` re-points the contact the same way.
# Get Company
Source: https://docs.cometly.com/api-reference/endpoint/get-company
GET https://app.cometly.com/public-api/v1/companies/{id}
Retrieve details of a specific company by its ID
## Overview
This endpoint allows you to retrieve the details of a company from Cometly.
## Path Parameters
The unique identifier of the company to retrieve
## Response
### Success Response
Returns the company object directly (not wrapped in a `data` property).
The unique identifier of the company
The domain associated with this company. `null` if the company was created from a customer-supplied external ID with no associated email domain, or if its domain claim was later released via [Update Company](/api-reference/endpoint/update-company).
The customer-supplied external ID associated with this company (your own account/company identifier). `null` if the company was created from an email domain alone, or if its external ID claim was later released via [Update Company](/api-reference/endpoint/update-company).
The name associated with this company
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
curl https://app.cometly.com/public-api/v1/companies/12345 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json"
```
```javascript JavaScript theme={null}
const companyId = 12345;
const response = await fetch(`https://app.cometly.com/public-api/v1/companies/${companyId}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const data = await response.json();
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
company_id = 12345
url = f'https://app.cometly.com/public-api/v1/companies/{company_id}'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.get(url, headers=headers)
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Company successfully retrieved |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Company not found |
| 422 | Invalid company ID provided |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **30 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
# Get Company Export
Source: https://docs.cometly.com/api-reference/endpoint/get-company-export
GET https://app.cometly.com/public-api/v1/companies/exports/{id}
Check the status of a company export job and retrieve the download URL when ready
## Overview
This endpoint retrieves the current status of a company export job created with the [Create Company Export](/api-reference/endpoint/create-company-export) endpoint. Once the export is complete, it provides a presigned download URL for the exported file.
Poll this endpoint regularly to check when your export is ready for download.
## Path Parameters
The export ID returned from the [Create Company Export](/api-reference/endpoint/create-company-export) endpoint.
Example: `123`
## Response
### Export Status Fields
The unique identifier for this export job
Current status of the export job
Possible values:
* `queued` - Export job is waiting to be processed
* `processing` - Export is currently being generated
* `completed` - Export is complete and ready for download
* `failed` - Export failed
ISO 8601 timestamp when the export was created
User-friendly status message (present when status is `queued`, `processing`, or `failed`)
### Additional Fields (When Status = `completed`)
Presigned S3 URL to download the export file. This URL expires after 15 minutes for security.
The file is in **gzipped NDJSON** format (Newline-Delimited JSON, compressed with gzip).
ISO 8601 timestamp when the export finished processing
ISO 8601 timestamp when the download URL will expire (15 minutes from request time)
## Example Requests
```bash cURL theme={null}
# Check export status
curl -G "https://app.cometly.com/public-api/v1/companies/exports/123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
```
```javascript JavaScript theme={null}
// Check export status
const exportId = 123;
const response = await fetch(`https://app.cometly.com/public-api/v1/companies/exports/${exportId}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
});
const exportData = await response.json();
console.log(`Status: ${exportData.status}`);
if (exportData.status === 'completed') {
console.log(`Download URL: ${exportData.download_url}`);
console.log(`Expires at: ${exportData.expires_at}`);
}
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
export_id = 123
base_url = f'https://app.cometly.com/public-api/v1/companies/exports/{export_id}'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
response = requests.get(base_url, headers=headers)
export_data = response.json()
print(f"Status: {export_data['status']}")
if export_data['status'] == 'completed':
print(f"Download URL: {export_data['download_url']}")
print(f"Expires at: {export_data['expires_at']}")
```
## Example Responses
### Queued Export
```json theme={null}
{
"export_id": 123,
"status": "queued",
"created_at": "2024-01-15T10:30:00Z",
"message": "Export is being processed. Please check back in a few minutes."
}
```
### Completed Export
```json theme={null}
{
"export_id": 123,
"status": "completed",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:33:42Z",
"download_url": "https://bucket.s3.amazonaws.com/exports/a3f2c8d9-1234-5678-9abc-def012345678.json.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
"expires_at": "2024-01-15T10:48:42Z"
}
```
### Failed Export
```json theme={null}
{
"export_id": 123,
"status": "failed",
"created_at": "2024-01-15T10:30:00Z",
"message": "Export failed. Please try again in a few minutes. If the problem persists, contact support."
}
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Export status retrieved successfully |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Export not found or belongs to a different space |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **30 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Download URL Expiration**: The presigned download URL expires **15 minutes** after you retrieve it. If it expires, make another GET request to this endpoint to get a fresh URL.
* **Space Isolation**: You can only access exports created by your space. Attempting to access another space's export returns 404.
* **File Security**: Export files use UUID-based filenames to prevent enumeration. Download URLs are cryptographically signed and time-limited.
* **Processing Time**: Exports typically complete within seconds to a few minutes. Large exports may take longer.
## See Also
* [Create Company Export](/api-reference/endpoint/create-company-export) - Create a new export job
* [List Companies](/api-reference/endpoint/list-companies) - Alternative for smaller datasets
# Get Contact
Source: https://docs.cometly.com/api-reference/endpoint/get-contact
GET https://app.cometly.com/public-api/v1/contacts/{id}
Retrieve details of a specific contact by its ID
## Overview
This endpoint allows you to retrieve the details of a contact from Cometly. The response includes all associated emails, phones, names, and locations for the contact, including data from merged profiles.
## Path Parameters
The unique identifier of the contact to retrieve
## Query Parameters
When set to `1`, includes the last 5 comet tokens associated with this contact, ordered by most recent first. Accepts `1` or `0`.
When set to `1`, includes the contact's full event journey — all conversions and touchpoints sorted by most recent first. Each event includes type (`conversion` or `touchpoint`), timestamps, URLs, and for touchpoints the full ad hierarchy (ad, ad set, campaign, account). Accepts `1` or `0`.
When set to `1` (default), direct touchpoints are excluded from the events list. Set to `0` to include them. Only applies when `include_events=1`. Accepts `1` or `0`.
When set to `1`, custom field keys in the `custom_fields` object will use the user-defined labels (e.g. `"Customer Age"`) instead of the raw column names (e.g. `"profile_field_1"`). Fields without a configured label will keep their raw column name. Accepts `1` or `0`.
When set to `1`, includes up to the last 1000 raw browsing session hits (page views) for this contact, ordered by most recent first. Accepts `1` or `0`.
## Response
### Success Response
Returns the contact object directly (not wrapped in a `data` property).
The unique identifier of the contact
Array of email addresses associated with this contact. Maximum 25 most recent. Ordered by most recent first.
Array of phone numbers associated with this contact. Maximum 25 most recent. Ordered by most recent first.
Array of names associated with this contact. Maximum 25 most recent. Ordered by most recent first.
Array of locations associated with this contact. Maximum 25 most recent. Ordered by most recent first.
Array of comet tokens associated with this contact. Maximum 5 most recent. Ordered by most recent first. Only included when `include_comet_tokens=true` is specified.
Array of the contact's conversions and touchpoints, sorted by most recent first. Only included when `include_events=1` is specified. Each object has a `type` field (`conversion` or `touchpoint`) and type-specific fields — see Notes for details.
Object containing all 30 custom fields. Fields 1-15 are text (string|null), 16-25 are numeric (number|null), 26-30 are date (string|null). Always included in the response.
Array of raw browsing session hits (page views) for this contact, ordered by most recent first. Capped at 1000 hits. Only included when `include_browsing_session_data=1` is specified. Each hit includes `event_url` (full URL — host, path, and query string combined), `referrer_host`, `source`, `country`, `device_type`, `browser`, `os`, `event_name`, and `event_time_utc`.
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
# Basic request
curl https://app.cometly.com/public-api/v1/contacts/12345 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json"
# Include comet tokens
curl https://app.cometly.com/public-api/v1/contacts/12345?include_comet_tokens=1 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json"
```
```javascript JavaScript theme={null}
const contactId = 12345;
// Basic request
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts/${contactId}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const data = await response.json();
// Include comet tokens
const responseWithTokens = await fetch(`https://app.cometly.com/public-api/v1/contacts/${contactId}?include_comet_tokens=1`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const dataWithTokens = await responseWithTokens.json();
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
contact_id = 12345
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
# Basic request
url = f'https://app.cometly.com/public-api/v1/contacts/{contact_id}'
response = requests.get(url, headers=headers)
# Include comet tokens
url_with_tokens = f'https://app.cometly.com/public-api/v1/contacts/{contact_id}'
params = {'include_comet_tokens': 1}
response_with_tokens = requests.get(url_with_tokens, headers=headers, params=params)
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Contact successfully retrieved |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Contact not found |
| 422 | Invalid contact ID provided |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **30 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Profile Merging**: If a contact has been merged with another contact, requesting the old contact ID will automatically return the current (merged) contact with all associated data from both profiles.
* **Array Limits**: Each array field (emails, phones, names, locations) returns a maximum of 25 most recent items to optimize performance.
* All arrays are ordered by most recent first (descending `created_at`).
* Empty arrays are returned if no data exists for a particular field.
* **Comet Tokens**: Use the `include_comet_tokens=1` query parameter to retrieve the last 5 comet tokens for the contact. This parameter is optional and defaults to `0`.
* **Custom Fields**: The response always includes a `custom_fields` object with all 30 custom fields. Fields 1-15 are text, 16-25 are numeric, 26-30 are date. Fields that have not been set will be `null`.
* **Custom Field Labels**: Use `use_custom_field_labels=1` to replace raw column names (`profile_field_1`) with user-defined labels (e.g. `Customer Age`) in the `custom_fields` object. Fields without a configured label will retain their raw column name.
* **Events**: Use `include_events=1` to retrieve the contact's full event journey. Events are sorted by most recent first and include both conversions and touchpoints:
* **Conversion fields**: `type` (`"conversion"`), `id`, `event_name`, `amount`, `event_time_utc`, `event_url`, `order_id`, `order_name`, `channel` (`"browser"` or `"server"`), and `integration` (only for server-side events, e.g. `"Shopify"`, `"Stripe"`)
* **Touchpoint fields**: `type` (`"touchpoint"`), `event_time_utc`, `source`, `touchpoint_url`, `referrer_url`, `ad_id`, `ad_name`, `adset_id`, `adset_name`, `campaign_id`, `campaign_name`, `account_id`, `account_name`
* Ad hierarchy fields (`ad_id`, `ad_name`, etc.) are resolved from the attributed ad and will be `null` for touchpoints without ad data (e.g. organic or direct).
* **Direct touchpoints** are hidden by default. Use `hide_direct_touchpoints=0` to include them.
* **Browsing Session Hits**: Use `include_browsing_session_data=1` to retrieve up to the last 1000 raw browsing hits for the contact, ordered most recent first. Each hit contains `event_url` (the full URL — host, path, and query string combined), `referrer_host`, `source`, `country`, `device_type`, `browser`, `os`, `event_name`, and `event_time_utc`. This is raw hit data, not grouped into sessions — group client-side if needed.
# Get Contact by Email
Source: https://docs.cometly.com/api-reference/endpoint/get-contact-by-email
GET https://app.cometly.com/public-api/v1/contacts/by-email/{email}
Retrieve a single contact by its email address
## Overview
This endpoint looks up exactly one contact by email address, when you already expect a single match. **The success response is byte-identical to [Get Contact](/api-reference/endpoint/get-contact)** — same fields (`id`, `emails`, `phones`, `names`, `locations`, `custom_fields`) and the same optional extras, all of which behave identically here. See [Get Contact](/api-reference/endpoint/get-contact) for the full field-by-field response breakdown; it isn't repeated on this page.
You can address the contact either of two ways:
* **By path**: `GET /contacts/by-email/{email}`
* **By query string**: `GET /contacts/by-email?email={email}`
Both forms resolve identically and use the same rate limit. The path form accepts either a raw `@` or a percent-encoded `%40` in the `{email}` segment.
Resolution matches the `email` filter on [List Contacts](/api-reference/endpoint/list-contacts): matching is exact (not partial or fuzzy) and case-insensitive, with surrounding whitespace trimmed. It matches **both** the contact's primary address and any of its alternate addresses, including addresses captured on profiles that were later merged into the contact.
Because this endpoint addresses a single contact, more than one match is an error rather than an arbitrary pick — see the `409` response below.
Use this endpoint when you expect exactly one contact for the address. Use [List Contacts](/api-reference/endpoint/list-contacts) with its `email` filter instead when you want to see every contact that shares an address, need to batch up to 100 addresses in one call, or want to avoid the possibility of a `409` entirely.
## Path Parameters
The email address of the contact to retrieve, when using the path form `GET /contacts/by-email/{email}`. Required when not using the query form. Accepts a raw `@` or a percent-encoded `%40`.
## Query Parameters
The email address of the contact to retrieve, when using the query form `GET /contacts/by-email?email=`. Required when not using the path form.
When set to `1`, includes the last 5 comet tokens associated with this contact, ordered by most recent first. Accepts `1` or `0`.
When set to `1`, includes the contact's full event journey — all conversions and touchpoints sorted by most recent first. Accepts `1` or `0`.
When set to `1` (default), direct touchpoints are excluded from the events list. Set to `0` to include them. Only applies when `include_events=1`. Accepts `1` or `0`.
When set to `1`, custom field keys in the `custom_fields` object will use the user-defined labels (e.g. `"Customer Age"`) instead of the raw column names (e.g. `"profile_field_1"`). Fields without a configured label will keep their raw column name. Accepts `1` or `0`.
When set to `1`, includes up to the last 1000 raw browsing session hits (page views) for this contact, ordered by most recent first. Accepts `1` or `0`.
## Response
### Success Response
Returns the contact object directly (not wrapped in a `data` property) — the same shape returned by [Get Contact](/api-reference/endpoint/get-contact), including all of its optional fields (`comet_tokens`, `events`, `browsing_session_hits`) under the same query parameters.
### Error Response
Error description explaining what went wrong.
Only present on a `409` response. The sorted list of canonical contact ids that matched the given email.
## Example Requests
### Look Up by Path
```bash cURL theme={null}
curl "https://app.cometly.com/public-api/v1/contacts/by-email/jane%40acme.com" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json"
```
```javascript JavaScript theme={null}
const email = 'jane@acme.com';
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts/by-email/${encodeURIComponent(email)}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const data = await response.json();
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
from urllib.parse import quote
email = 'jane@acme.com'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
url = f'https://app.cometly.com/public-api/v1/contacts/by-email/{quote(email, safe="")}'
response = requests.get(url, headers=headers)
data = response.json()
```
### Look Up by Query String
```bash cURL theme={null}
curl -G "https://app.cometly.com/public-api/v1/contacts/by-email" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d "email=jane@acme.com"
```
```javascript JavaScript theme={null}
const email = 'jane@acme.com';
const params = new URLSearchParams({ email });
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts/by-email?${params}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const data = await response.json();
```
```php PHP theme={null}
$email]);
$headers = [
'Authorization: Bearer YOUR_API_KEY',
'Accept: application/json',
'Content-Type: application/json'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
email = 'jane@acme.com'
url = 'https://app.cometly.com/public-api/v1/contacts/by-email'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.get(url, headers=headers, params={'email': email})
data = response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Exactly one contact matched and was retrieved |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | No contact matches the given email |
| 409 | More than one contact matches the given email — see `contact_ids` in the response |
| 422 | Invalid parameters (missing or malformed email) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **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](/api-reference/endpoint/list-contacts). See [Rate Limiting](/introduction/rate-limiting) for details.
* **Resolution**: Matching is exact and case-insensitive, with surrounding whitespace trimmed. It matches **both** the contact's primary address and any of its alternate addresses, including addresses captured on profiles that were later merged into the contact — the identical algorithm used by the `email` filter on [List Contacts](/api-reference/endpoint/list-contacts).
* **Multiple matches are a `409`, not a pick**: several distinct contacts can legitimately share an address. When that happens, nothing is guessed — the response is `{"message": "Multiple contacts match this email. Fetch them with GET /contacts?email= and then request one by its contact id.", "contact_ids": [...]}`. Either call [List Contacts](/api-reference/endpoint/list-contacts) with its `email` filter to see all of the matches, or request one of the returned `contact_ids` directly via [Get Contact](/api-reference/endpoint/get-contact).
* **No match is a `404`**: unlike the list-based `email` filter (which returns a normal `200` with an empty array), this endpoint returns `404` with `{"message": "Contact not found"}` when nothing matches, because it addresses a single resource.
* **Same payload as Get Contact**: the response body, including all optional fields and query parameters, is identical to [Get Contact](/api-reference/endpoint/get-contact). Nothing here can drift from that endpoint's documented shape.
# Get Contact Export
Source: https://docs.cometly.com/api-reference/endpoint/get-contact-export
GET https://app.cometly.com/public-api/v1/contacts/exports/{id}
Check the status of a contact export job and retrieve the download URL when ready
## Overview
This endpoint retrieves the current status of a contact export job created with the [Create Contact Export](/api-reference/endpoint/create-contact-export) endpoint. Once the export is complete, it provides a presigned download URL for the exported file.
Poll this endpoint regularly to check when your export is ready for download.
## Path Parameters
The export ID returned from the [Create Contact Export](/api-reference/endpoint/create-contact-export) endpoint.
Example: `123`
## Response
### Export Status Fields
The unique identifier for this export job
Current status of the export job
Possible values:
* `queued` - Export job is waiting to be processed
* `processing` - Export is currently being generated
* `completed` - Export is complete and ready for download
* `failed` - Export failed
ISO 8601 timestamp when the export was created
User-friendly status message (present when status is `queued`, `processing`, or `failed`)
### Additional Fields (When Status = `completed`)
Presigned S3 URL to download the export file. This URL expires after 15 minutes for security.
The file is in **gzipped NDJSON** format (Newline-Delimited JSON, compressed with gzip).
ISO 8601 timestamp when the export finished processing
ISO 8601 timestamp when the download URL will expire (15 minutes from request time)
## Example Requests
```bash cURL theme={null}
# Check export status
curl -G "https://app.cometly.com/public-api/v1/contacts/exports/123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
```
```javascript JavaScript theme={null}
// Check export status
const exportId = 123;
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts/exports/${exportId}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
});
const exportData = await response.json();
console.log(`Status: ${exportData.status}`);
if (exportData.status === 'completed') {
console.log(`Download URL: ${exportData.download_url}`);
console.log(`Expires at: ${exportData.expires_at}`);
}
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
export_id = 123
base_url = f'https://app.cometly.com/public-api/v1/contacts/exports/{export_id}'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
response = requests.get(base_url, headers=headers)
export_data = response.json()
print(f"Status: {export_data['status']}")
if export_data['status'] == 'completed':
print(f"Download URL: {export_data['download_url']}")
print(f"Expires at: {export_data['expires_at']}")
```
## Example Responses
### Queued Export
```json theme={null}
{
"export_id": 123,
"status": "queued",
"created_at": "2024-01-15T10:30:00Z",
"message": "Export is being processed. Please check back in a few minutes."
}
```
### Completed Export
```json theme={null}
{
"export_id": 123,
"status": "completed",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:33:42Z",
"download_url": "https://bucket.s3.amazonaws.com/exports/a3f2c8d9-1234-5678-9abc-def012345678.json.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
"expires_at": "2024-01-15T10:48:42Z"
}
```
### Failed Export
```json theme={null}
{
"export_id": 123,
"status": "failed",
"created_at": "2024-01-15T10:30:00Z",
"message": "Export failed. Please try again in a few minutes. If the problem persists, contact support."
}
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Export status retrieved successfully |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Export not found or belongs to a different space |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **30 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Download URL Expiration**: The presigned download URL expires **15 minutes** after you retrieve it. If it expires, make another GET request to this endpoint to get a fresh URL.
* **Space Isolation**: You can only access exports created by your space. Attempting to access another space's export returns 404.
* **File Security**: Export files use UUID-based filenames to prevent enumeration. Download URLs are cryptographically signed and time-limited.
* **Processing Time**: Exports typically complete within seconds to a few minutes. Large exports may take longer.
## See Also
* [Create Contact Export](/api-reference/endpoint/create-contact-export) - Create a new export job
* [List Contacts](/api-reference/endpoint/list-contacts) - Alternative for smaller datasets
# Get Event
Source: https://docs.cometly.com/api-reference/endpoint/get-event
GET https://app.cometly.com/public-api/v1/events/{event_id}
Retrieve details of a specific event by its ID
## Overview
This endpoint allows you to retrieve the details of a previously created event from Cometly.
## Path Parameters
The unique identifier of the event to retrieve
## Response
### Success Response
Returns the event object directly (not wrapped in a `data` property).
The unique identifier of the event
The identifier of the contact profile associated with this event
Your company identifier
The space identifier this event belongs to
The name of the event (e.g., `purchase`, `custom_event_1`)
The configured name of the event. For custom events (`custom_event_1` through `custom_event_50`), this is the user-configured name. For standard events, this equals `event_name`.
Transaction amount
Indicates if this is an upsell transaction
Timestamp of when the event occurred (UTC)
Order identifier
Order name or description
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
curl https://app.cometly.com/public-api/v1/events/123456 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json"
```
```javascript JavaScript theme={null}
const eventId = '123456';
const response = await fetch(`https://app.cometly.com/public-api/v1/events/${eventId}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const data = await response.json();
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
event_id = '123456'
url = f'https://app.cometly.com/public-api/v1/events/{event_id}'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.get(url, headers=headers)
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Event successfully retrieved |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Event not found |
| 422 | Invalid event ID provided |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **30 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
# Get Event Export
Source: https://docs.cometly.com/api-reference/endpoint/get-event-export
GET https://app.cometly.com/public-api/v1/events/exports/{id}
Check the status of an export job and retrieve the download URL when ready
## Overview
This endpoint retrieves the current status of an export job created with the [Create Event Export](/api-reference/endpoint/create-event-export) endpoint. Once the export is complete, it provides a presigned download URL for the exported file.
Poll this endpoint regularly to check when your export is ready for download.
## Path Parameters
The export ID returned from the [Create Event Export](/api-reference/endpoint/create-event-export) endpoint.
Example: `123`
## Response
### Export Status Fields
The unique identifier for this export job
Current status of the export job
Possible values:
* `queued` - Export job is waiting to be processed
* `processing` - Export is currently being generated
* `completed` - Export is complete and ready for download
* `failed` - Export failed
ISO 8601 timestamp when the export was created
User-friendly status message (present when status is `queued`, `processing`, or `failed`)
### Additional Fields (When Status = `completed`)
Presigned S3 URL to download the export file. This URL expires after 15 minutes for security.
The file is in **gzipped NDJSON** format (Newline-Delimited JSON, compressed with gzip).
ISO 8601 timestamp when the export finished processing
ISO 8601 timestamp when the download URL will expire (15 minutes from request time)
## Example Requests
```bash cURL theme={null}
# Check export status
curl -G "https://app.cometly.com/public-api/v1/events/exports/123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json"
```
```javascript JavaScript theme={null}
// Check export status
const exportId = 123;
const response = await fetch(`https://app.cometly.com/public-api/v1/events/exports/${exportId}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
});
const exportData = await response.json();
console.log(`Status: ${exportData.status}`);
if (exportData.status === 'completed') {
console.log(`Download URL: ${exportData.download_url}`);
console.log(`Expires at: ${exportData.expires_at}`);
}
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
# Check export status
export_id = 123
base_url = f'https://app.cometly.com/public-api/v1/events/exports/{export_id}'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json'
}
response = requests.get(base_url, headers=headers)
export_data = response.json()
print(f"Status: {export_data['status']}")
if export_data['status'] == 'completed':
print(f"Download URL: {export_data['download_url']}")
print(f"Expires at: {export_data['expires_at']}")
```
## Example Responses
### Queued Export
```json theme={null}
{
"export_id": 123,
"status": "queued",
"created_at": "2024-01-15T10:30:00Z",
"message": "Export is being processed. Please check back in a few minutes."
}
```
### Processing Export
```json theme={null}
{
"export_id": 123,
"status": "processing",
"created_at": "2024-01-15T10:30:00Z",
"message": "Export is being processed. Please check back in a few minutes."
}
```
### Completed Export
```json theme={null}
{
"export_id": 123,
"status": "completed",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:33:42Z",
"download_url": "https://bucket.s3.amazonaws.com/exports/a3f2c8d9-1234-5678-9abc-def012345678.json.gz?X-Amz-Algorithm=AWS4-HMAC-SHA256&...",
"expires_at": "2024-01-15T10:48:42Z"
}
```
### Failed Export
```json theme={null}
{
"export_id": 123,
"status": "failed",
"created_at": "2024-01-15T10:30:00Z",
"message": "Export failed. Please try again in a few minutes. If the problem persists, contact support."
}
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Export status retrieved successfully |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Export not found or belongs to a different space |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **30 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Download URL Expiration**: The presigned download URL expires **15 minutes** after you retrieve it. If it expires, make another GET request to this endpoint to get a fresh URL.
* **Space Isolation**: You can only access exports created by your space. Attempting to access another space's export returns 404.
* **File Security**: Export files use UUID-based filenames to prevent enumeration. Download URLs are cryptographically signed and time-limited.
* **Processing Time**: Exports typically complete within seconds to a few minutes. Large exports (100,000+ events) may take longer.
* **Retry Logic**: If an export fails, create a new export job rather than retrying the same one.
* **Storage**: Export files are stored for retrieval but should be downloaded promptly. Don't rely on long-term storage of export files in Cometly's system.
## See Also
* [Create Event Export](/api-reference/endpoint/create-event-export) - Create a new export job
* [Exports vs Pagination](/concepts/exports-vs-pagination) - Understand when to use exports
* [List Events](/api-reference/endpoint/list-events) - Alternative for smaller datasets
# List Companies
Source: https://docs.cometly.com/api-reference/endpoint/list-companies
GET https://app.cometly.com/public-api/v1/companies
Retrieve a paginated list of companies with optional date filtering
## Overview
This endpoint allows you to retrieve a list of companies from Cometly. Results are cursor-paginated for efficient data retrieval and ordered by creation date (newest first).
## Query Parameters
### Optional Parameters
Number of companies to return per page. Minimum: 1, Maximum: 5000
Pagination cursor from a previous response. Use this to fetch the next page of results.
Filter companies created on or after this date. Format: `YYYY-MM-DD HH:MM:SS`. The timestamp is interpreted in your space's timezone.
Example: `2024-01-01 00:00:00`
Filter companies created on or before this date. Format: `YYYY-MM-DD HH:MM:SS`. Must be after start\_date. The timestamp is interpreted in your space's timezone.
Example: `2024-01-31 23:59:59`
## Response
### Success Response
The response follows Laravel's cursor pagination structure:
Array of company objects. Each company includes:
* `id` (integer): The unique identifier of the company
* `domain` (string | null): The domain associated with this company. `null` if the company was created from a customer-supplied external ID with no associated email domain, or if its domain claim was later released via [Update Company](/api-reference/endpoint/update-company).
* `external_id` (string | null): The customer-supplied external ID associated with this company (your own account/company identifier). `null` if the company was created from an email domain alone, or if its external ID claim was later released via [Update Company](/api-reference/endpoint/update-company).
The base URL path for the endpoint
Number of items per page
Cursor for the next page of results. `null` if there are no more pages.
Full URL for the next page of results. `null` if there are no more pages.
Cursor for the previous page of results. `null` if on the first page.
Full URL for the previous page of results. `null` if on the first page.
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
# Basic request without filters
curl -G "https://app.cometly.com/public-api/v1/companies" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json"
# Request with date filtering
curl -G "https://app.cometly.com/public-api/v1/companies" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-01 00:00:00" \
--data-urlencode "end_date=2024-01-31 23:59:59" \
-d "per_page=100"
# Pagination request using cursor
curl -G "https://app.cometly.com/public-api/v1/companies" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d "cursor=eyJjcmVhdGVkX2F0IjoiMjAyNC0wMS0xNSAxMj..."
```
```javascript JavaScript theme={null}
// Basic request without filters
const response = await fetch('https://app.cometly.com/public-api/v1/companies', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const data = await response.json();
// Request with date filtering
const params = new URLSearchParams({
start_date: '2024-01-01 00:00:00',
end_date: '2024-01-31 23:59:59',
per_page: '100'
});
const filteredResponse = await fetch(`https://app.cometly.com/public-api/v1/companies?${params}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const filteredData = await filteredResponse.json();
// Pagination using cursor from previous response
if (filteredData.next_cursor) {
const nextParams = new URLSearchParams({
cursor: filteredData.next_cursor
});
const nextPage = await fetch(`https://app.cometly.com/public-api/v1/companies?${nextParams}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
}
```
```php PHP theme={null}
'2024-01-01 00:00:00',
'end_date' => '2024-01-31 23:59:59',
'per_page' => 100
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $params);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
curl_close($ch);
// Pagination using cursor
if (!empty($data['next_cursor'])) {
$nextParams = http_build_query([
'cursor' => $data['next_cursor']
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $nextParams);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$nextResponse = curl_exec($ch);
curl_close($ch);
}
?>
```
```python Python theme={null}
import requests
# Basic request without filters
base_url = 'https://app.cometly.com/public-api/v1/companies'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.get(base_url, headers=headers)
data = response.json()
# Request with date filtering
params = {
'start_date': '2024-01-01 00:00:00',
'end_date': '2024-01-31 23:59:59',
'per_page': 100
}
response = requests.get(base_url, headers=headers, params=params)
data = response.json()
# Pagination using cursor from previous response
if data.get('next_cursor'):
next_params = {
'cursor': data['next_cursor']
}
next_response = requests.get(base_url, headers=headers, params=next_params)
next_data = next_response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Companies successfully retrieved |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **15 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* Results are ordered by creation date in descending order (newest first)
* Cursor pagination is used for efficient traversal of large result sets
* Date filtering is optional. When omitted, all companies are returned.
* Dates are provided in your space's configured timezone and converted to UTC for querying
* Only company `id`, `domain`, and `external_id` fields are returned in the response — `name` is not included in the list view; use [Get Company](/api-reference/endpoint/get-company) to retrieve it
# List Contacts
Source: https://docs.cometly.com/api-reference/endpoint/list-contacts
GET https://app.cometly.com/public-api/v1/contacts
Retrieve a paginated list of contacts filtered by creation date or by email address
## Overview
This endpoint allows you to retrieve a paginated list of contacts from Cometly within a specific date range, by email address, or both. Each contact includes primary data from the profile (email, name, phone, location). For complete contact information including all associated emails, phones, names, and locations from merged profiles, use the individual contact endpoint GET /contacts/. Results are cursor-paginated for efficient data retrieval and ordered by ID (newest first).
## Query Parameters
### Filter Parameters
Filter contacts by email address. Accepts a single address (`email=jane@acme.com`), a comma-separated list (`email=a@x.com,b@y.com`), or a repeated array parameter (`email[]=a@x.com&email[]=b@y.com`). Maximum **100** addresses per request.
Matching is exact (not partial or fuzzy) and case-insensitive; surrounding whitespace is trimmed. It matches **both** the contact's primary address and any of its alternate addresses, including addresses 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. Several distinct contacts can legitimately share an address; all of them are returned.
You can combine `email` with `start_date` / `end_date` — when both are present, a contact must match the email **and** fall inside the creation window.
**Mapping results back to what you asked for:** a contact matched on one of its alternate addresses is returned with its *primary* address in `email`, which is a different value from the one you queried. Without `include_all_emails=1` the address you searched for appears nowhere in the row, so batched lookups cannot be joined back to your input list. Pass `include_all_emails=1` whenever you send more than one address.
Start date and time for filtering contacts by creation date in `YYYY-MM-DD HH:MM:SS` format. The date is interpreted in your space's timezone.
Required unless `email` is supplied. When present, `end_date` must also be present — sending only one returns a `422`.
Example: `2024-01-15 00:00:00`
End date and time for filtering contacts by creation date in `YYYY-MM-DD HH:MM:SS` format. Must be after `start_date`. The date is interpreted in your space's timezone.
Required unless `email` is supplied. When present, `start_date` must also be present — sending only one returns a `422`.
Example: `2024-01-15 23:59:59`
### Optional Parameters
Number of contacts to return per page. Minimum: 1, Maximum: 5000
Pagination cursor from a previous response. Use this to fetch the next page of results.
When set to `1`, includes the last 5 comet tokens for each contact, ordered by most recent first. Accepts `1` or `0`.
When set to `1`, includes all email addresses associated with the contact (including emails from merged profiles). Accepts `1` or `0`.
When set to `1`, includes all 30 custom field columns for each contact. Fields 1-15 are text, 16-25 are numeric, 26-30 are date. Accepts `1` or `0`.
When set to `1`, custom field keys in the response will use the user-defined labels (e.g. `"Customer Age"`) instead of the raw column names (e.g. `"profile_field_1"`). Only applies when `include_custom_fields=1`. Fields without a configured label will keep their raw column name. Accepts `1` or `0`.
Filter contacts by their custom profile field values. Array of AND-joined conditions. Each condition is `{ "field": "profile_field_N", "operator": "", "value": }`, or for ranges `{ "field": "profile_field_N", "operator": "between", "start": , "end": }`. The `field` must reference a custom field slot that has been configured with a label in your space (e.g. `profile_field_5`). Maximum 10 conditions per request.
**Operator vocabulary by field type:**
| Slot range | Type | Valid operators |
| - | - | - |
| `profile_field_1` – `profile_field_15` | text | `equal_to`, `not_equal_to`, `contains`, `not_contains`, `starts_with`, `ends_with`, `contains_word`, `any`, `unknown` |
| `profile_field_16` – `profile_field_25` | number | `equal_to`, `not_equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `any`, `unknown` |
| `profile_field_26` – `profile_field_30` | date | `equal_to`, `between`, `greater_than`, `less_than`, `age_greater_than`, `age_equal_to`, `age_less_than`, `any`, `unknown` |
`any` matches contacts where the field has a value; `unknown` matches contacts where the field is empty or unset. For **numeric fields**, `any` requires a value greater than `0` — `unknown` matches `0` or unset. **Date comparisons** (`equal_to`, `greater_than`, `less_than`, `between`) are evaluated in your space's configured timezone; `equal_to` matches the full calendar day. `not_equal_to` and `not_contains` do not include contacts where the field is unset — add a separate `unknown` condition if you need those.
Example query string: `custom_field_filters[0][field]=profile_field_5&custom_field_filters[0][operator]=equal_to&custom_field_filters[0][value]=Premium`
## Response
### Success Response
The response follows Laravel's cursor pagination structure:
Array of contact objects. Each contact includes:
* `id` (integer): The unique identifier of the contact
* `email` (string|null): Primary email address
* `name` (string|null): Primary name
* `phone` (string|null): Primary phone number
* `location` (string|null): Primary location
* `comet_tokens` (array): Array of comet tokens (only included when `include_comet_tokens=1`)
* `emails` (array): Array of all email addresses associated with the contact, including emails from merged profiles (only included when `include_all_emails=1`)
* `profile_field_1` through `profile_field_30`: Custom field values (only included when `include_custom_fields=1`). Fields 1-15 are text (string|null), 16-25 are numeric (number|null), 26-30 are date (string|null)
The base URL path for the endpoint
Number of items per page
Cursor for the next page of results. `null` if there are no more pages.
Full URL for the next page of results. `null` if there are no more pages.
Cursor for the previous page of results. `null` if on the first page.
Full URL for the previous page of results. `null` if on the first page.
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
# Basic request
curl -G "https://app.cometly.com/public-api/v1/contacts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-15 00:00:00" \
--data-urlencode "end_date=2024-01-15 23:59:59"
# Include comet tokens
curl -G "https://app.cometly.com/public-api/v1/contacts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-15 00:00:00" \
--data-urlencode "end_date=2024-01-15 23:59:59" \
-d "include_comet_tokens=1"
# Include all emails
curl -G "https://app.cometly.com/public-api/v1/contacts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-15 00:00:00" \
--data-urlencode "end_date=2024-01-15 23:59:59" \
-d "include_all_emails=1"
# Look up contacts by email (no date range required; batch up to 100 addresses)
curl -G "https://app.cometly.com/public-api/v1/contacts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d "email=jane@acme.com,john@acme.com"
# Filter by custom field value
curl -G "https://app.cometly.com/public-api/v1/contacts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-15 00:00:00" \
--data-urlencode "end_date=2024-01-15 23:59:59" \
-d "custom_field_filters[0][field]=profile_field_5" \
-d "custom_field_filters[0][operator]=equal_to" \
-d "custom_field_filters[0][value]=Premium"
# Pagination request using cursor
curl -G "https://app.cometly.com/public-api/v1/contacts" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-15 00:00:00" \
--data-urlencode "end_date=2024-01-15 23:59:59" \
-d "cursor=eyJpZCI6MTIzNDU..."
```
```javascript JavaScript theme={null}
// Basic request
const params = new URLSearchParams({
start_date: '2024-01-15 00:00:00',
end_date: '2024-01-15 23:59:59'
});
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts?${params}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const data = await response.json();
// Include comet tokens
const paramsWithTokens = new URLSearchParams({
start_date: '2024-01-15 00:00:00',
end_date: '2024-01-15 23:59:59',
include_comet_tokens: 1
});
// Include all emails
const paramsWithEmails = new URLSearchParams({
start_date: '2024-01-15 00:00:00',
end_date: '2024-01-15 23:59:59',
include_all_emails: 1
});
const responseWithEmails = await fetch(`https://app.cometly.com/public-api/v1/contacts?${paramsWithEmails}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const dataWithEmails = await responseWithEmails.json();
// Look up contacts by email (no date range required; batch up to 100 addresses)
const paramsWithEmailFilter = new URLSearchParams({
email: 'jane@acme.com,john@acme.com'
});
const responseWithEmailFilter = await fetch(`https://app.cometly.com/public-api/v1/contacts?${paramsWithEmailFilter}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const dataWithEmailFilter = await responseWithEmailFilter.json();
const responseWithTokens = await fetch(`https://app.cometly.com/public-api/v1/contacts?${paramsWithTokens}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const dataWithTokens = await responseWithTokens.json();
// Filter by custom field value
const paramsWithFilters = new URLSearchParams();
paramsWithFilters.append('start_date', '2024-01-15 00:00:00');
paramsWithFilters.append('end_date', '2024-01-15 23:59:59');
paramsWithFilters.append('custom_field_filters[0][field]', 'profile_field_5');
paramsWithFilters.append('custom_field_filters[0][operator]', 'equal_to');
paramsWithFilters.append('custom_field_filters[0][value]', 'Premium');
const responseWithFilters = await fetch(`https://app.cometly.com/public-api/v1/contacts?${paramsWithFilters}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const dataWithFilters = await responseWithFilters.json();
// Pagination using cursor from previous response
if (data.next_cursor) {
const nextParams = new URLSearchParams({
start_date: '2024-01-15 00:00:00',
end_date: '2024-01-15 23:59:59',
cursor: data.next_cursor
});
const nextPage = await fetch(`https://app.cometly.com/public-api/v1/contacts?${nextParams}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
}
```
```php PHP theme={null}
'2024-01-15 00:00:00',
'end_date' => '2024-01-15 23:59:59'
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $params);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
curl_close($ch);
// Include comet tokens
$paramsWithTokens = http_build_query([
'start_date' => '2024-01-15 00:00:00',
'end_date' => '2024-01-15 23:59:59',
'include_comet_tokens' => 1
]);
// Include all emails
$paramsWithEmails = http_build_query([
'start_date' => '2024-01-15 00:00:00',
'end_date' => '2024-01-15 23:59:59',
'include_all_emails' => 1
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $paramsWithEmails);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$responseWithEmails = curl_exec($ch);
$dataWithEmails = json_decode($responseWithEmails, true);
curl_close($ch);
// Look up contacts by email (no date range required; batch up to 100 addresses)
$paramsWithEmailFilter = http_build_query([
'email' => 'jane@acme.com,john@acme.com'
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $paramsWithEmailFilter);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$responseWithEmailFilter = curl_exec($ch);
$dataWithEmailFilter = json_decode($responseWithEmailFilter, true);
curl_close($ch);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $paramsWithTokens);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$responseWithTokens = curl_exec($ch);
$dataWithTokens = json_decode($responseWithTokens, true);
curl_close($ch);
// Filter by custom field value
$paramsWithFilters = http_build_query([
'start_date' => '2024-01-15 00:00:00',
'end_date' => '2024-01-15 23:59:59',
'custom_field_filters' => [
[
'field' => 'profile_field_5',
'operator' => 'equal_to',
'value' => 'Premium',
],
],
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $paramsWithFilters);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$responseWithFilters = curl_exec($ch);
$dataWithFilters = json_decode($responseWithFilters, true);
curl_close($ch);
// Pagination using cursor
if (!empty($data['next_cursor'])) {
$nextParams = http_build_query([
'start_date' => '2024-01-15 00:00:00',
'end_date' => '2024-01-15 23:59:59',
'cursor' => $data['next_cursor']
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $nextParams);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$nextResponse = curl_exec($ch);
curl_close($ch);
}
?>
```
```python Python theme={null}
import requests
# Basic request
base_url = 'https://app.cometly.com/public-api/v1/contacts'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
params = {
'start_date': '2024-01-15 00:00:00',
'end_date': '2024-01-15 23:59:59'
}
response = requests.get(base_url, headers=headers, params=params)
data = response.json()
# Include comet tokens
params_with_tokens = {
'start_date': '2024-01-15 00:00:00',
'end_date': '2024-01-15 23:59:59',
'include_comet_tokens': 1
}
# Include all emails
params_with_emails = {
'start_date': '2024-01-15 00:00:00',
'end_date': '2024-01-15 23:59:59',
'include_all_emails': 1
}
response_with_emails = requests.get(base_url, headers=headers, params=params_with_emails)
data_with_emails = response_with_emails.json()
response_with_tokens = requests.get(base_url, headers=headers, params=params_with_tokens)
data_with_tokens = response_with_tokens.json()
# Look up contacts by email (no date range required; batch up to 100 addresses)
params_with_email_filter = {
'email': 'jane@acme.com,john@acme.com'
}
response_with_email_filter = requests.get(base_url, headers=headers, params=params_with_email_filter)
data_with_email_filter = response_with_email_filter.json()
# Filter by custom field value
params_with_filters = {
'start_date': '2024-01-15 00:00:00',
'end_date': '2024-01-15 23:59:59',
'custom_field_filters[0][field]': 'profile_field_5',
'custom_field_filters[0][operator]': 'equal_to',
'custom_field_filters[0][value]': 'Premium',
}
response_with_filters = requests.get(base_url, headers=headers, params=params_with_filters)
data_with_filters = response_with_filters.json()
# Pagination using cursor from previous response
if data.get('next_cursor'):
next_params = {
'start_date': '2024-01-15 00:00:00',
'end_date': '2024-01-15 23:59:59',
'cursor': data['next_cursor']
}
next_response = requests.get(base_url, headers=headers, params=next_params)
next_data = next_response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Contacts successfully retrieved |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **15 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Primary Data Only**: This endpoint returns primary contact data (email, name, phone, location) from the profiles table for efficient browsing.
* **Complete Contact Data**: For full contact information including all associated emails, phones, names, and locations from merged profiles, use GET /contacts/.
* **Performance**: This endpoint uses a single efficient query, making it ideal for listing and browsing large numbers of contacts.
* **Ordering**: Results are ordered by ID in descending order (newest first).
* **Pagination**: Cursor pagination is used for efficient traversal of large result sets. `next_page_url` preserves whichever `email` form (single address, comma-separated list, or `email[]` array) you used on the original request.
* **Comet Tokens**: Use the `include_comet_tokens=1` query parameter to include the last 5 comet tokens for each contact. This parameter is optional and defaults to `0`.
* **All Emails**: Use the `include_all_emails=1` query parameter to include all email addresses for each contact, including emails from merged profiles. This parameter is optional and defaults to `0`.
* **Custom Fields**: Use the `include_custom_fields=1` query parameter to include all 30 custom fields in the response. Fields 1-15 are text, 16-25 are numeric, 26-30 are date. This parameter is optional and defaults to `0`.
* **Custom Field Labels**: Use `use_custom_field_labels=1` alongside `include_custom_fields=1` to replace raw column names (`profile_field_1`) with user-defined labels (e.g. `Customer Age`). Fields without a configured label will retain their raw column name.
* **Custom Field Filters**: Use `custom_field_filters` to narrow results to contacts whose custom profile fields match specified values. Conditions AND together; up to 10 per request. Slots that haven't been configured with a label in your space cannot be filtered against and will be rejected with a 422. Age operators (`age_greater_than`, `age_equal_to`, `age_less_than`) accept a whole-number day count and are supported on date fields (`profile_field_26`–`profile_field_30`). The `_` and `\` characters in text filter values are treated as literals, not LIKE wildcards.
* **Email Lookup**: Use the `email` query parameter to look up contacts by address instead of, or in addition to, a creation-date range. Matching is exact and case-insensitive (with surrounding whitespace trimmed) — not partial or fuzzy. It matches **both** the contact's primary address and any of its alternate addresses, including addresses 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.
* **Shared Addresses**: More than one distinct contact can legitimately share the same email address; `email` returns all of them.
* **Expecting a single contact?** If you expect exactly one contact for an address and want a `404`/`409` instead of inspecting an array, use [Get Contact by Email](/api-reference/endpoint/get-contact-by-email) (`GET /contacts/by-email/{email}` or `GET /contacts/by-email?email=`) instead.
* **No Match Is Still a 200**: A request with no matching contacts (whether filtering by `email`, a date range, or both) returns a normal `200` with an empty `data` array — never a `404`, since this is a list endpoint.
* **Batch Email Lookups**: Because this endpoint is rate-limited to 15 requests per minute per space, batch up to 100 addresses into a single `email` request rather than issuing one call per address when reconciling a list of emails in bulk. Pair batched lookups with `include_all_emails=1`: a contact matched on an alternate address is returned with its primary address in `email`, so without the `emails` array there is no way to tell which of your input addresses produced a given row.
# List Events
Source: https://docs.cometly.com/api-reference/endpoint/list-events
GET https://app.cometly.com/public-api/v1/events
Retrieve a paginated list of events with optional attribution filtering
## Overview
This endpoint allows you to retrieve a list of events from Cometly with powerful filtering capabilities including date ranges, attribution models, sources, and field selection. Results are cursor-paginated for efficient data retrieval.
## Query Parameters
### Required Parameters
Start date for the query range in `YYYY-MM-DD HH:MM:SS` format. The timestamp is interpreted in your space's timezone.
Example: `2024-01-01 00:00:00`
End date for the query range in `YYYY-MM-DD HH:MM:SS` format. Must be after start\_date and is interpreted in your space's timezone.
Example: `2024-01-31 23:59:59`
### Optional Parameters
Determines whether to report metrics by contact or company level.
Options:
* `profile` - Contact-level events (default)
* `company` - Company-level events
Filter results to specific event names. Defaults to all events when omitted.
**Limits:** Minimum 1, maximum 10 event names. Duplicate values are not allowed.
Values:
* `add_payment_info`
* `add_to_cart`
* `complete_registration`
* `contact`
* `initiate_checkout`
* `lead_generated`
* `purchase`
* `schedule`
* `sign_up`
* `start_trial`
* `submit_application`
* `subscribe`
* `upsell_purchase`
* `view_content`
* `webinar_registration`
Custom events: `custom_event_1` through `custom_event_50`.
Filter events by traffic sources. When provided, `attribution_model` and `attribution_window` become required.
**Limits:** Minimum 1, maximum 15 sources. Duplicate values are not allowed.
**Paid Ads:**
`facebook_ads`, `google_ads`, `linkedin_ads`, `tiktok_ads`, `youtube_ads`, `instagram_ads`, `bing_ads`, `yahoo_ads`, `pinterest_ads`, `reddit_ads`, `snapchat_ads`, `telegram_ads`, `twitch_ads`, `x_ads`, `quora_ads`, `duckduckgo_ads`, `whop_ads`
**Organic:**
`facebook_organic`, `google_organic`, `linkedin_organic`, `tiktok_organic`, `youtube_organic`, `instagram_organic`, `bing_organic`, `yahoo_organic`, `pinterest_organic`, `reddit_organic`, `snapchat_organic`, `telegram_organic`, `twitch_organic`, `x_organic`, `quora_organic`, `duckduckgo_organic`, `messenger_organic`, `brave_organic`, `threads_organic`
**AI Platforms:**
`ai_chat_gpt`, `ai_perplexity`, `ai_copilot`, `ai_claude`, `ai_grok`, `ai_gemini`, `ai_deepseek`
**Other:**
`email`, `sms`, `referral`, `direct`, `shop_app`, `substack`, `capterra`, `trustpilot`, `g2`, `yelp`
Attribution model to use for filtering events. Required when `sources` is provided.
Options:
* `first_touch` - First interaction attribution
* `last_touch` - Last interaction attribution
* `linear` - Equal credit across all touchpoints
* `first_platform_touch` - First paid platform touch
* `last_platform_touch` - Last paid platform touch
* `linear_paid` - Linear attribution across paid touchpoints only
* `u_shaped` - 40% first, 40% last, 20% middle touchpoints
* `last_non_direct_touch` - Last touch excluding direct traffic
Attribution lookback window in days. Required when `sources` is provided.
Options: `0` (LTV), `1`, `7`, `14`, `30`, `60`, `90`
Specify which fields to include in the response.
**Limits:** Minimum 1, maximum 20 fields. Duplicate values are not allowed.
**Note:** `id` and `event_time_utc` are always included in the response. When `sources` are provided for attribution, `touchpoint_id` is also always included. These fields are required for cursor pagination to work properly.
Available fields:
* `profile_id` - Contact profile identifier
* `company_id` - Company identifier
* `space_id` - Space identifier
* `event_name` - Event name
* `configured_name` - The configured name of the event. For custom events (`custom_event_1` through `custom_event_50`), this is the user-configured name. For standard events, this equals `event_name`.
* `amount` - Transaction amount (with attribution, this is the partial amount credited to the touchpoint)
* `is_upsell` - Upsell flag
* `ad_id` - Platform-specific ad identifier (requires `sources` to be provided)
* `ad_name` - Ad name from the ad platform (requires `sources` to be provided)
* `adset_id` - Platform-specific ad set identifier (requires `sources` to be provided)
* `adset_name` - Ad set name from the ad platform (requires `sources` to be provided)
* `campaign_id` - Platform-specific campaign identifier (requires `sources` to be provided)
* `campaign_name` - Campaign name from the ad platform (requires `sources` to be provided)
* `account_id` - Platform-specific ad account identifier (requires `sources` to be provided)
* `account_name` - Ad account name from the ad platform (requires `sources` to be provided)
* `order_id` - Order identifier
* `order_name` - Order name
* `event_url` - The full URL where the event occurred
**Auto-included fields (not user-requestable):**
* `id` - Event identifier (always included for cursor pagination)
* `event_time_utc` - Event timestamp in UTC (always included for cursor pagination)
* `touchpoint_id` - Touchpoint identifier (automatically included when sources are provided for cursor pagination)
* `source` - Traffic source (automatically included when any ad hierarchy field is requested: `ad_id`, `ad_name`, `adset_id`, `adset_name`, `campaign_id`, `campaign_name`, `account_id`, `account_name`)
Number of events to return per page. Minimum: 1, Maximum: 4000
Pagination cursor from a previous response. Use this to fetch the next page of results.
Filter events by their owning contact's custom profile field values. Array of AND-joined conditions, each `{ "field": "profile_field_N", "operator": "", "value": }`, or for ranges `{ "field": "profile_field_N", "operator": "between", "start": , "end": }`. The `field` must reference a custom field slot configured with a label in your space (e.g. `profile_field_5`). Same shape and operator vocabulary as the [List Contacts](/api-reference/endpoint/list-contacts) endpoint. Maximum 10 conditions per request.
**Operator vocabulary by field type:**
| Slot range | Type | Valid operators |
| - | - | - |
| `profile_field_1` – `profile_field_15` | text | `equal_to`, `not_equal_to`, `contains`, `not_contains`, `starts_with`, `ends_with`, `contains_word`, `any`, `unknown` |
| `profile_field_16` – `profile_field_25` | number | `equal_to`, `not_equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `any`, `unknown` |
| `profile_field_26` – `profile_field_30` | date | `equal_to`, `between`, `greater_than`, `less_than`, `age_greater_than`, `age_equal_to`, `age_less_than`, `any`, `unknown` |
`any` matches contacts where the field has a value; `unknown` matches contacts where the field is empty or unset. For **numeric fields**, `any` requires a value greater than `0` — `unknown` matches `0` or unset. **Date comparisons** (`equal_to`, `greater_than`, `less_than`, `between`) are evaluated in your space's configured timezone; `equal_to` matches the full calendar day. `not_equal_to` and `not_contains` do not include contacts where the field is unset — add a separate `unknown` condition if you need those.
Example query string: `custom_field_filters[0][field]=profile_field_5&custom_field_filters[0][operator]=equal_to&custom_field_filters[0][value]=Premium`
## Response
### Success Response
The response follows Laravel's cursor pagination structure:
Array of event objects with the requested fields
The base URL path for the endpoint
Number of items per page
Cursor for the next page of results. `null` if there are no more pages.
Full URL for the next page of results. `null` if there are no more pages.
Cursor for the previous page of results. `null` if on the first page.
Full URL for the previous page of results. `null` if on the first page.
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
# Basic request with only required parameters
curl -G "https://app.cometly.com/public-api/v1/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-01 00:00:00" \
--data-urlencode "end_date=2024-01-31 23:59:59"
# Request with attribution filtering and specific fields
curl -G "https://app.cometly.com/public-api/v1/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-01 00:00:00" \
--data-urlencode "end_date=2024-01-31 23:59:59" \
-d "query_mode=profile" \
-d "event_names[]=purchase" \
-d "event_names[]=sign_up" \
-d "sources[]=facebook_ads" \
-d "sources[]=google_ads" \
-d "attribution_model=last_touch" \
-d "attribution_window=30" \
-d "fields[]=id" \
-d "fields[]=event_name" \
-d "fields[]=amount" \
-d "fields[]=event_time_utc" \
-d "per_page=100"
# Filter events by the owning contact's custom field value
curl -G "https://app.cometly.com/public-api/v1/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-01 00:00:00" \
--data-urlencode "end_date=2024-01-31 23:59:59" \
-d "event_names[]=purchase" \
-d "custom_field_filters[0][field]=profile_field_5" \
-d "custom_field_filters[0][operator]=equal_to" \
-d "custom_field_filters[0][value]=Premium"
# Pagination request using cursor
curl -G "https://app.cometly.com/public-api/v1/events" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-urlencode "start_date=2024-01-01 00:00:00" \
--data-urlencode "end_date=2024-01-31 23:59:59" \
-d "cursor=eyJldmVudF90aW1lX3V0YyI6IjIwMjQtMDEtMTUgMTI..."
```
```javascript JavaScript theme={null}
// Basic request with only required parameters
const params = new URLSearchParams({
start_date: '2024-01-01 00:00:00',
end_date: '2024-01-31 23:59:59'
});
const response = await fetch(`https://app.cometly.com/public-api/v1/events?${params}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const data = await response.json();
// Request with attribution filtering and specific fields
const advancedParams = new URLSearchParams({
start_date: '2024-01-01 00:00:00',
end_date: '2024-01-31 23:59:59',
query_mode: 'profile',
attribution_model: 'last_touch',
attribution_window: '30',
per_page: '100'
});
// Add array parameters
['purchase', 'sign_up'].forEach(eventName =>
advancedParams.append('event_names[]', eventName)
);
['facebook_ads', 'google_ads'].forEach(source =>
advancedParams.append('sources[]', source)
);
['id', 'event_name', 'amount', 'event_time_utc'].forEach(field =>
advancedParams.append('fields[]', field)
);
const advancedResponse = await fetch(`https://app.cometly.com/public-api/v1/events?${advancedParams}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const advancedData = await advancedResponse.json();
// Pagination using cursor from previous response
if (advancedData.next_cursor) {
const nextParams = new URLSearchParams({
start_date: '2024-01-01 00:00:00',
end_date: '2024-01-31 23:59:59',
cursor: advancedData.next_cursor
});
const nextPage = await fetch(`https://app.cometly.com/public-api/v1/events?${nextParams}`, {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
}
```
```php PHP theme={null}
'2024-01-01 00:00:00',
'end_date' => '2024-01-31 23:59:59'
]);
$headers = [
'Authorization: Bearer YOUR_API_KEY',
'Accept: application/json',
'Content-Type: application/json'
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $params);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
curl_close($ch);
// Request with attribution filtering and specific fields
$advancedParams = [
'start_date' => '2024-01-01 00:00:00',
'end_date' => '2024-01-31 23:59:59',
'query_mode' => 'profile',
'event_names' => ['purchase', 'sign_up'],
'sources' => ['facebook_ads', 'google_ads'],
'attribution_model' => 'last_touch',
'attribution_window' => 30,
'fields' => ['id', 'event_name', 'amount', 'event_time_utc'],
'per_page' => 100
];
$queryString = http_build_query($advancedParams);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $queryString);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$data = json_decode($response, true);
curl_close($ch);
// Pagination using cursor
if (!empty($data['next_cursor'])) {
$nextParams = http_build_query([
'start_date' => '2024-01-01 00:00:00',
'end_date' => '2024-01-31 23:59:59',
'cursor' => $data['next_cursor']
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $baseUrl . '?' . $nextParams);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$nextResponse = curl_exec($ch);
curl_close($ch);
}
?>
```
```python Python theme={null}
import requests
from urllib.parse import urlencode
# Basic request with only required parameters
base_url = 'https://app.cometly.com/public-api/v1/events'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
params = {
'start_date': '2024-01-01 00:00:00',
'end_date': '2024-01-31 23:59:59'
}
response = requests.get(base_url, headers=headers, params=params)
data = response.json()
# Request with attribution filtering and specific fields
advanced_params = {
'start_date': '2024-01-01 00:00:00',
'end_date': '2024-01-31 23:59:59',
'query_mode': 'profile',
'event_names[]': ['purchase', 'sign_up'],
'sources[]': ['facebook_ads', 'google_ads'],
'attribution_model': 'last_touch',
'attribution_window': 30,
'fields[]': ['id', 'event_name', 'amount', 'event_time_utc'],
'per_page': 100
}
response = requests.get(base_url, headers=headers, params=advanced_params)
data = response.json()
# Pagination using cursor from previous response
if data.get('next_cursor'):
next_params = {
'start_date': '2024-01-01 00:00:00',
'end_date': '2024-01-31 23:59:59',
'cursor': data['next_cursor']
}
next_response = requests.get(base_url, headers=headers, params=next_params)
next_data = next_response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Events successfully retrieved |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **7 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* When `sources` are provided, attribution filtering is applied and only events matching the attribution criteria are returned
* **Attribution Breakdown:** With multi-touch attribution models (linear, u\_shaped, linear\_paid), each event may return **multiple rows** - one per attributed touchpoint. Each row contains the touchpoint's partial credit (e.g., a \$100 conversion with 4 touchpoints returns 4 rows with \$25 each). Single-touch models (first\_touch, last\_touch) return one row per event.
* When `sources` are not provided, all events in the specified date range are returned without attribution filtering (one row per event)
* The ad hierarchy fields (`ad_id`, `ad_name`, `adset_id`, `adset_name`, `campaign_id`, `campaign_name`, `account_id`, `account_name`) require `sources` to be provided. Requesting higher-level hierarchy fields (such as `account_name`) may increase processing time compared to requesting only lower-level fields (such as `ad_name`), so selecting only the fields you need will generally be more efficient.
* Cursor pagination is used for efficient traversal of large result sets
* The `id` and `event_time_utc` fields are always included in responses. When attribution is used (sources provided), `touchpoint_id` is also always included.
* Dates are provided in your space's configured timezone
* By default, only `id` and `event_time_utc` are returned (plus `touchpoint_id` when using attribution). Specify additional fields as needed to minimize response size.
* **Custom Field Filters**: Use `custom_field_filters` to narrow results to events whose owning contact matches conditions on custom profile fields. Filters are AND-joined; up to 10 per request. Slots that haven't been configured with a label in your space are rejected with a 422. Age operators (`age_greater_than`, `age_equal_to`, `age_less_than`) accept a whole-number day count and are supported on date fields. The `_` and `\` characters in text filter values are treated as literals, not LIKE wildcards.
# Update Company
Source: https://docs.cometly.com/api-reference/endpoint/update-company
PUT https://app.cometly.com/public-api/v1/companies/{id}
Update an existing company's domain, external ID, or name
## Overview
This endpoint updates an existing company using **sparse PUT** semantics. Any field you include is replaced with the value you provide; any field you omit is left untouched.
## Path Parameters
The unique identifier of the company to update.
## Request Body
At least one of `domain`, `external_id`, or `name` must be provided.
New domain for the company. **This is an identifier, not just a label**: it decides which company future events from that email domain are matched to. The value is trimmed and lowercased before storing, matching ingestion — a blank string is treated as `null`. Omit the key to leave the existing domain untouched.
Sending `null` (or blank) releases the company's domain claim: the company keeps its `id`, history, and contacts, but future events from that domain will create or match a different company instead. Re-sending the current domain is a no-op `200`.
If the new domain already belongs to another company in the Space, the request is rejected with `409` and nothing is changed — see [Error Response](#error-response) below. Maximum 255 characters.
New external ID for the company — your own account/company identifier, the same value you send as `company_external_id` on events. **This is an identifier, not just a label**: it decides which company future events carrying that `company_external_id` are matched to. Unlike `domain`, it is stored byte-exact: trimmed, but case is **preserved** — `ACCT_1` and `acct_1` are two different ids. 1 to 190 characters after trimming; a blank string is treated as `null`. Integers are accepted and stored as their decimal string (e.g. `1234567`). Omit the key to leave the existing external ID untouched.
Sending `null` (or blank) releases the company's claim on the external ID: the company keeps its `id`, history, and contacts, but future events carrying that `company_external_id` will create or match a different company instead. Re-sending the current external ID is a no-op `200`.
If the new external ID already belongs to another company in the Space, the request is rejected with `409` and nothing is changed — see [Error Response](#error-response) below. Floats, arrays, and booleans are rejected with a `422`.
Ingestion only groups by `company_external_id` when the Space's company identity mode (Space Settings → Additional Setup → Company Tracking) is set to **External ID**. In the default **Auto** mode, the value you set here is stored and returned by this API, but event ingestion groups companies by email domain and ignores `company_external_id`.
New name for the company. Omit to leave the existing name untouched. Maximum 255 characters. Cannot be `null` — the stored value is a required column, so sending `name: null` returns a `422`.
## Response
### Success Response
Returns the full company object after the update.
The unique identifier of the company.
The domain currently associated with this company. `null` if the company has no domain claim — either it was created from a customer-supplied external ID alone, or its domain was released via this endpoint.
The external ID currently associated with this company. `null` if the company has no external ID claim — either it was created from an email domain alone, or its external ID was released via this endpoint.
The name currently associated with this company.
### Error Response
Error description explaining what went wrong.
On a `409` conflict, the response also includes:
The identifier field that caused the conflict: `domain` or `external_id`.
The ID of the company in this Space that already owns the given `domain` or `external_id` (see `field`). Re-point contacts to that company, or update it directly, instead of retrying this request.
## Example Requests
```bash cURL theme={null}
curl -X PUT "https://app.cometly.com/public-api/v1/companies/12345" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"domain": "acme.io",
"external_id": "acct_8f3k2",
"name": "Acme Inc"
}'
```
```javascript JavaScript theme={null}
const companyId = 12345;
const response = await fetch(`https://app.cometly.com/public-api/v1/companies/${companyId}`, {
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
domain: 'acme.io',
external_id: 'acct_8f3k2',
name: 'Acme Inc'
})
});
const data = await response.json();
console.log('Updated company:', data);
```
```php PHP theme={null}
'acme.io',
'external_id' => 'acct_8f3k2',
'name' => 'Acme Inc',
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
company_id = 12345
url = f'https://app.cometly.com/public-api/v1/companies/{company_id}'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'domain': 'acme.io',
'external_id': 'acct_8f3k2',
'name': 'Acme Inc',
}
response = requests.put(url, headers=headers, json=data)
result = response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Company successfully updated |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Company not found |
| 409 | The given `domain` or `external_id` already belongs to another company in this Space. Nothing is changed. |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **60 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Sparse PUT semantics**: Only the fields you include in the request body are modified. Other columns are left untouched.
* **At least one field is required**: A request with none of `domain`, `external_id`, or `name` is rejected with a 422 error.
* **`domain` and `external_id` are identifiers, not just labels**: `domain` determines which company future events from that email domain are matched to; `external_id` determines which company future events carrying that `company_external_id` are matched to. Changing either moves the company's claim on that identifier away from wherever it lived before.
* **Domains are normalized, external IDs are byte-exact**: `domain` values are trimmed and lowercased before storing, to match how the tracker resolves domains during ingestion. `external_id` values are trimmed but case is **preserved** — `ACCT_1` and `acct_1` are different ids — 1 to 190 characters after trimming; integers are accepted and stored as their decimal string. A blank string is treated as `null` for either field.
* **`domain: null` or `external_id: null` releases the claim**: the company keeps its `id`, history, and contacts, but future events matching that identifier will create or match a different company. Re-sending the current value is a no-op `200`.
* **Identifier conflicts are rejected, not merged**: if the new `domain` or `external_id` already belongs to another company in the Space, the request fails with `409` and `conflicting_company_id` naming the owner, with `field` naming which identifier conflicted. Nothing is written. Re-point contacts to the existing company, or update it directly, instead of retrying.
* **`external_id` only affects ingestion in External ID mode**: event ingestion only groups by `company_external_id` when the Space's company identity mode (Space Settings → Additional Setup → Company Tracking) is set to **External ID**. In the default **Auto** mode, the value set here is stored and returned by this API but ignored for grouping.
* **`name` cannot be cleared**: unlike `domain` and `external_id`, `name` is a required column in the tracker. Sending `name: null` returns a `422` — omit the key instead if you don't want to change it.
# Update Contact
Source: https://docs.cometly.com/api-reference/endpoint/update-contact
PUT https://app.cometly.com/public-api/v1/contacts/{id}
Update an existing contact's emails, phones, names, locations, or custom fields
## Overview
This endpoint updates an existing contact using **sparse PUT** semantics. Any field you include in the request body is treated as the full, authoritative list for that collection — existing values are deleted and replaced with the values you provide. Any field you omit is left untouched.
This design lets you change one aspect of a contact (e.g. add a phone number) without having to send the contact's entire state. However, you must include the **complete list** for any field you choose to update — sending `emails: ["a@x.com"]` will remove every other email already on the contact.
## Path Parameters
The unique identifier of the contact to update. Merged profile aliases are automatically resolved to the current canonical profile.
## Request Body
At least one of `emails`, `phones`, `names`, `locations`, or `custom_fields` must be provided.
Full replacement list of email addresses. Provide the complete authoritative array — anything not included is removed. Provide `[]` to wipe all emails. The first item becomes the primary email on the contact's profile.
Full replacement list of phone numbers. Provide `[]` to wipe all phones. The first item becomes the primary phone.
Full replacement list of names. Provide `[]` to wipe all names. The first item becomes the primary name.
Full replacement list of locations. Provide `[]` to wipe all locations. The first item becomes the primary location.
Full replacement object of custom profile fields keyed by field slug (`profile_field_1` through `profile_field_30`). When provided, every key NOT included is set to `null`. Provide `{}` to wipe all 30 custom fields. Fields 1-15 accept text (max 10,240 chars), 16-25 accept numbers, 26-30 accept dates.
## Response
### Success Response
Returns the updated contact with only the fields that were modified. Omitted request fields are not echoed back.
The unique identifier of the contact (after resolving merged-profile aliases).
The new list of emails, if `emails` was provided in the request.
The new list of phone numbers, if `phones` was provided in the request.
The new list of names, if `names` was provided in the request.
The new list of locations, if `locations` was provided in the request.
The new custom fields object, if `custom_fields` was provided in the request.
### Error Response
Error description explaining what went wrong.
## Example Requests
```bash cURL theme={null}
curl -X PUT "https://app.cometly.com/public-api/v1/contacts/12345" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"emails": ["john@example.com", "john.doe@example.com"],
"phones": ["+1234567890"],
"custom_fields": {
"profile_field_1": "Enterprise",
"profile_field_16": "500"
}
}'
```
```javascript JavaScript theme={null}
const contactId = 12345;
const response = await fetch(`https://app.cometly.com/public-api/v1/contacts/${contactId}`, {
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
emails: ['john@example.com', 'john.doe@example.com'],
phones: ['+1234567890'],
custom_fields: {
profile_field_1: 'Enterprise',
profile_field_16: '500'
}
})
});
const data = await response.json();
console.log('Updated contact:', data);
```
```php PHP theme={null}
['john@example.com', 'john.doe@example.com'],
'phones' => ['+1234567890'],
'custom_fields' => [
'profile_field_1' => 'Enterprise',
'profile_field_16' => '500',
],
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
contact_id = 12345
url = f'https://app.cometly.com/public-api/v1/contacts/{contact_id}'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'emails': ['john@example.com', 'john.doe@example.com'],
'phones': ['+1234567890'],
'custom_fields': {
'profile_field_1': 'Enterprise',
'profile_field_16': '500',
},
}
response = requests.put(url, headers=headers, json=data)
result = response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Contact successfully updated |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Contact not found |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **60 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Sparse PUT semantics**: Omitted fields are NOT modified. Provided fields are treated as the complete authoritative state for that collection.
* **To add a value without losing existing data**, first call [Get Contact](/api-reference/endpoint/get-contact) to retrieve the current array, then PUT the merged list.
* **Custom fields are all-or-nothing**: providing a `custom_fields` object sets ALL 30 columns — keys not in the object are cleared to `null`. To preserve unchanged fields, include them in the object.
* **Empty arrays wipe a collection**: `phones: []` removes every phone row for the contact and sets the primary phone to `null`.
* **Merged profiles**: If you pass an ID that was merged into another profile, the update is applied to the canonical (current) profile.
* **Transaction Safety**: Updates are wrapped in a database transaction to ensure data consistency.
# Update Event
Source: https://docs.cometly.com/api-reference/endpoint/update-event
PUT https://app.cometly.com/public-api/v1/events/{event_id}
Update fields on an existing event
## Overview
This endpoint updates fields on an event that Cometly has already ingested. It uses **sparse PUT** semantics: fields you include are replaced, fields you omit are left as they are. At least one updatable field must be provided.
Currently `amount` is the updatable field.
## Path Parameters
The unique identifier of the event to update — the same `id` returned by [List Events](/api-reference/endpoint/list-events) or [Get Event](/api-reference/endpoint/get-event).
## Request Body
The new transaction amount. Must be `0` or greater. Numeric strings such as `"120.25"` are accepted. Sending `null` clears the amount.
## Response
### Success Response
Confirmation message, e.g. `"Event updated successfully"`
### Error Response
Error description explaining what went wrong
## Example Requests
```bash cURL theme={null}
curl -X PUT https://app.cometly.com/public-api/v1/events/123456 \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"amount": 49.50
}'
```
```javascript JavaScript theme={null}
const eventId = '123456';
const response = await fetch(`https://app.cometly.com/public-api/v1/events/${eventId}`, {
method: 'PUT',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
amount: 49.50
})
});
const data = await response.json();
console.log(data.message); // "Event updated successfully"
```
```php PHP theme={null}
49.50,
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$result = json_decode($response, true);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
event_id = '123456'
url = f'https://app.cometly.com/public-api/v1/events/{event_id}'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'amount': 49.50,
}
response = requests.put(url, headers=headers, json=data)
result = response.json()
```
## Status Codes
| Status Code | Description |
| - | - |
| 200 | Event successfully updated |
| 401 | Missing or invalid API key |
| 403 | API key doesn't have permission or subscription is inactive |
| 404 | Event not found in this Space |
| 422 | Invalid parameters provided (check error message for details) |
| 429 | Too many requests - rate limit exceeded. See [Rate Limiting](/introduction/rate-limiting) |
## Notes
* **Rate Limit**: This endpoint has a limit of **60 requests per minute** per Space. See [Rate Limiting](/introduction/rate-limiting) for details.
* **Unknown fields** in the request body are ignored.
# Changelog
Source: https://docs.cometly.com/changelog
Track changes, new features, and improvements to the Cometly API.
## v1.4.1
### September 29, 2026
**Contacts dataset: `tracking_ids` column** — the [Contacts dataset](/data-warehouse/datasets/contacts) has a new optional column, `tracking_ids`.
* `tracking_ids` is an uncapped JSON array of strings: every distinct `tracking_id` sent on the contact's events, including values inherited from contacts merged into it. It's `[]` when none, and off by default.
* It sits in the Identity graph group, next to `emails`, `phones`, and `comet_tokens`.
* Existing exports are unaffected. Create a new Contacts export with the column enabled to get it.
***
## v1.4.0
### September 23, 2026
**New endpoint: Update Event** — [`PUT /events/{id}`](/api-reference/endpoint/update-event) updates fields on an already-ingested event using sparse PUT semantics.
* `amount` is the updatable field today: `number | null`, `>= 0`; numeric strings like `"120.25"` are accepted, and `null` clears the amount. At least one updatable field must be provided.
* Returns `200` with `{ "message": "Event updated successfully" }`, and `404` when the id does not exist in the caller's Space.
* Rate limited at **60 requests per minute** per Space, the same bucket as [Update Contact](/api-reference/endpoint/update-contact) and [Update Company](/api-reference/endpoint/update-company).
* New MCP tool [`update_event`](/mcp/tools/update-event) mirrors the endpoint. It requires `amount` and cannot clear it to `null`.
***
## 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/](/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/](/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
# Comet Token
Source: https://docs.cometly.com/concepts/comet-token
Learn how to use the cometToken() function to improve attribution accuracy when sending events via the API
## Overview
The `cometToken()` function is available through the Cometly tracking pixel and returns a unique identifier that links a user's browser session to their attribution data. When sending events via the API, including the comet token significantly improves attribution accuracy.
## Using cometToken()
The `cometToken()` function returns a string value directly:
```javascript theme={null}
// Get the comet token - returns immediately
const token = cometToken();
console.log(token); // "ddffd8d8338383883838484843838388d8df8f"
```
## Sending Events with Comet Token
When sending events to the API, include the `comet_token` parameter for better attribution:
```javascript theme={null}
const token = cometToken();
const response = await fetch('https://app.cometly.com/public-api/v1/events/track', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
event_name: 'purchase',
email: 'customer@example.com',
comet_token: token,
amount: 99.99
})
});
```
## Prerequisites
The Cometly tracking pixel must be installed on your website for `cometToken()` to be available.
## Common Use Cases
### Form Submissions
Capture the comet token when a form is submitted:
```javascript theme={null}
document.getElementById('signup-form').addEventListener('submit', (e) => {
const formData = new FormData(e.target);
formData.append('comet_token', cometToken());
fetch('/api/signup', {
method: 'POST',
body: formData
});
});
```
### OAuth Sign Up (Google, Apple, etc.)
When users sign up via OAuth providers, Cometly's automatic form capture doesn't work. You must capture the comet token when the user clicks the OAuth button and include it when sending the event. See [Tracking Events When Using OAuth Sign Up](https://help.cometly.com/en/articles/9898157-tracking-events-when-using-oauth-sign-up-with-google) for details.
## See Also
* [Create Event](/api-reference/endpoint/create-event) - API endpoint for sending events
* [Cometly Help Center](https://help.cometly.com) - Installation guides and troubleshooting
# Exports vs Pagination
Source: https://docs.cometly.com/concepts/exports-vs-pagination
Learn when to use async exports versus paginated list endpoints for retrieving large datasets
## Overview
Cometly's API offers two methods for retrieving event data: **real-time paginated lists** and **asynchronous exports**.
| Feature | Paginated List | Async Export |
| - | - | - |
| **Data Volume** | Up to 4,000 per request | Entire day |
| **Format** | JSON (paginated) | NDJSON (gzipped) |
| **Use Case** | Real-time, interactive | Bulk, batch processing |
| **Rate Limit** | 7 requests/min | 5 creates/min, 30 status checks/min |
| **Date Range** | Flexible (any range) | Max 1 day per export |
| **Best For** | \< 10,000 events | > 10,000 events |
## Export File Format
Exports are delivered as **gzipped NDJSON** (Newline-Delimited JSON):
```json theme={null}
{"id":123,"event_name":"purchase","amount":99.99,"event_time_utc":"2024-01-15T10:23:45Z"}
{"id":124,"event_name":"sign_up","amount":0,"event_time_utc":"2024-01-15T10:24:12Z"}
```
Each line is a complete JSON object, making it easy to stream and process line-by-line without loading the entire file into memory.
## See Also
* [List Events](/api-reference/endpoint/list-events) - Real-time paginated endpoint
* [Create Event Export](/api-reference/endpoint/create-event-export) - Async bulk export
# Sending Data to Cometly
Source: https://docs.cometly.com/concepts/sending-data-to-cometly
Learn the two ways to send event data to Cometly: webhooks and the API
## Overview
There are two ways to send event data to Cometly:
| Method | Best For | Complexity |
| - | - | - |
| **Webhooks** | Most integrations | Low - no code required |
| **API** | Custom implementations | Medium - requires development |
## Webhooks (Recommended)
Webhooks are the easiest way to send data to Cometly. They enable automated, real-time data transmission from external applications without writing any code.
### Benefits
* **No code required** - Configure entirely through the Cometly UI
* **Works with most apps** - Any platform that supports webhooks (HubSpot, Stripe, ClickFunnels, HighLevel, and many more)
* **Built-in field mapping** - Match your app's data fields to Cometly through a visual interface
* **Data filtering** - Process only relevant events based on criteria you define
* **Data transformation** - Format dates, convert currencies, and perform calculations automatically
### How It Works
1. Get a unique webhook URL from Cometly (Space Setup → Add Events)
2. Add the URL to your external application's webhook settings
3. Map your app's fields to Cometly fields
4. Activate the webhook
See [Cometly Webhooks Integration](https://help.cometly.com/en/articles/10204379-cometly-webhooks-integration) for setup instructions.
## API
The API is for custom implementations where you need full control over when and how events are sent.
### When to Use the API
* Building a custom integration not supported by webhooks
* Server-side event tracking
* Programmatic event creation from your own application
* Bulk data operations via exports
### Getting Started
1. [Generate an API key](/introduction/authentication)
2. Use the [Create Event](/api-reference/endpoint/create-event) endpoint to send events
3. Include identification parameters (`email`, `comet_token`, or `fingerprint` + `ip`)
## Which Should I Use?
**Use webhooks** if your platform supports them. They're faster to set up, require no code, and include built-in data transformation features.
Use the API when:
* Your platform doesn't support webhooks
* You need to send events from server-side code
* You're building a custom application that requires programmatic control
# Set up a BigQuery destination
Source: https://docs.cometly.com/data-warehouse/bigquery-destination
Grant Cometly's service account access to a BigQuery dataset you own so Cometly can load export data directly into a table.
## Overview
A **BigQuery destination** is a reusable connection to a BigQuery dataset you own. Like a Snowflake
destination — and unlike an S3 or GCS destination — Cometly **loads the data directly into a table in
your warehouse** on each run.
Unlike Snowflake, you paste **no credentials at all**. Instead, you grant Cometly's service account
access on your dataset and prove you control it with a label. Cometly's load jobs run against your
project, but the jobs themselves are free — you only pay for storage.
## Prerequisites
* A Google Cloud project with a BigQuery dataset you own (or permission to create one), in the **US**
or **EU** multi-region.
* A space administrator role in Cometly.
## Connect Cometly to BigQuery
In BigQuery, create a dataset (or choose an existing one) for Cometly exports.
The dataset's location must be the **US** or **EU** multi-region. Cometly copies each run to a
transfer bucket colocated with your dataset before loading — a BigQuery requirement — and only
those two multi-regions are supported today. A dataset in any other location (including single
regions like `us-central1`) is rejected when you add the destination.
In BigQuery, go to your dataset → **Sharing → Permissions → Add principal**, and grant the
**BigQuery Data Editor** role to Cometly's loader service account. The exact service account email
is shown in the **Add destination** dialog in Cometly, with a copy button.
Add a label to the dataset with key `cometly-verify` and the value shown in the **Add destination**
dialog in Cometly (also copyable). This is a per-space token — it proves that the person connecting
the destination actually controls the dataset, so Cometly refuses to load into a dataset you don't
own.
In the dashboard, go to **Integrations → BigQuery → Add destination** and enter:
A label for this destination, for example "Production warehouse".
The GCP **project ID** (not the project display name or project number), for example
`my-company-prod`.
The BigQuery dataset you granted access to and labeled above, for example `cometly_exports`.
Cometly runs a connection check on save, and it's strict — the destination isn't created until it
passes.
The connection check confirms the dataset exists, that the service account has access, that the
`cometly-verify` label matches, and that the dataset's location is supported — each failure returns
a specific, actionable error rather than a generic one.
## Target table
When you create an export against this destination, Cometly creates the **target table** for you from
your column selection — an explicit schema with one column per selected field, all **NULLABLE**, typed
from the dataset's source: numeric columns become `INT64`, `NUMERIC`, or `FLOAT64`, dates become
`DATE`, and text becomes `STRING`. Time columns become `TIMESTAMP` (UTC) or, for zone-naive source
columns, `DATETIME` (values still UTC). JSON-array columns (for example the contacts identity-graph columns) are stored
as `STRING` containing raw JSON text; use `JSON_EXTRACT` or `PARSE_JSON` on your side to work with
them. Table names must start with a letter or underscore, followed by letters, digits, or underscores —
the `$` character allowed in Snowflake table names isn't valid here.
If a table with that name already exists in the dataset, creation fails and you're asked to choose a
different name. Cometly never creates a table by inference from a load, and never drops a table.
## Loads are append-only
Each run is a BigQuery batch **load job** (`WRITE_APPEND`) — free on your side; you only pay for the
storage the loaded rows occupy. Cometly does not de-duplicate on your behalf: like every recurring
export, a run re-exports its **entire trailing window** on each firing (see
[Cadence and windows](/data-warehouse/overview#cadence-and-windows)), so rows from a previous run's
overlapping window land again in the table. The number of rows loaded by each run is recorded on the
export's run history.
## De-duplicating loaded rows
Because runs append rather than upsert, collapse repeated rows with a `ROW_NUMBER()` window function
partitioned on the dataset's row-identity column (see the dataset's page — usually `id`, or `id` plus
`metric_date` for ad-analytics datasets):
```sql theme={null}
SELECT *
FROM `my-project.cometly_exports.touchpoints`
QUALIFY ROW_NUMBER() OVER (PARTITION BY id) = 1;
```
Save this as a view for query-time de-duplication, or run it as a scheduled query that physically
compacts the table:
```sql theme={null}
CREATE OR REPLACE TABLE `my-project.cometly_exports.touchpoints` AS
SELECT *
FROM `my-project.cometly_exports.touchpoints`
QUALIFY ROW_NUMBER() OVER (PARTITION BY id) = 1;
```
Schedule the second query (for example, daily, after your export's run) in BigQuery's
[scheduled queries](https://cloud.google.com/bigquery/docs/scheduling-queries) so the table stays
compact between reads.
For datasets whose rows can change between runs (ad analytics metrics, conversions updated within
the window), the copies of a row are not identical and the query above keeps an arbitrary one. If
you need strict latest-wins semantics for those datasets, load into an intermediate table and `MERGE` on
the identity column after each run, updating every non-key column.
## Next step
Choose this destination, a dataset, a target table name, and a date range.
# Create an export
Source: https://docs.cometly.com/data-warehouse/create-export
Configure a dataset, cadence, window, and columns, and load the data into one of your destinations.
## Overview
An **export** writes one [dataset](/data-warehouse/overview#datasets) to one of your destinations, on a schedule you choose — as Parquet files for an [S3](/data-warehouse/s3-destination) or [GCS](/data-warehouse/gcs-destination) destination, or loaded directly into a table for a [Snowflake](/data-warehouse/snowflake-destination) or [BigQuery](/data-warehouse/bigquery-destination) destination. This guide covers the create flow that's common to every dataset; each dataset page documents the columns and any dataset-specific settings.
## Prerequisites
* An S3, GCS, Snowflake, or BigQuery destination configured. See [Set up an S3 destination](/data-warehouse/s3-destination), [Set up a Google Cloud Storage destination](/data-warehouse/gcs-destination), [Set up a Snowflake destination](/data-warehouse/snowflake-destination), or [Set up a BigQuery destination](/data-warehouse/bigquery-destination).
* A space administrator role in Cometly.
## Create the export
In the dashboard, go to **Integrations → Amazon S3**, **Integrations → Google Cloud Storage**,
**Integrations → Snowflake**, or **Integrations → BigQuery** — the **Exports** table lists every
export regardless of destination type, and appears on all four pages. Select **Create export**.
Give it a recognizable name, for example "Touchpoints to warehouse".
Select the destination to write to. The dropdown only lists destinations matching the integration
page you opened it from — Amazon S3 destinations from **Integrations → Amazon S3**, Google Cloud
Storage destinations from **Integrations → Google Cloud Storage**, Snowflake destinations from
**Integrations → Snowflake**, BigQuery destinations from **Integrations → BigQuery**.
For an S3 or GCS destination, optionally set a path prefix (for example `cometly/`) — files are
written under this path within the bucket. For a Snowflake or BigQuery destination, name the
**target table** Cometly creates and loads into: it must start with a letter or underscore,
followed by any number of letters, digits, or underscores. Snowflake table names may also contain
`$`; BigQuery table names may not.
Choose the dataset to export. Each dataset has its own columns and quirks — see the
[dataset reference](/data-warehouse/overview#datasets).
Pick which columns the export carries from the dataset's catalog. Locked columns are always
included; the rest are optional. Your selection is **frozen at creation** (see below).
Choose how often the export runs and how much data each run covers:
* **One-time** — runs once. Pick an absolute **start and end date**.
* **Daily** — runs once a day at a **scheduled hour** you choose. Pick a **trailing window**
preset (up to 90 days).
* **Hourly** — runs every hour. Pick a **trailing window** preset (up to 14 days; up to 7 days
for ad-analytics datasets).
See [Cadence and windows](/data-warehouse/overview#cadence-and-windows) for how trailing windows
behave.
Some datasets ask for more at creation — the [Attributed conversions](/data-warehouse/datasets/attributed-conversions)
dataset takes an attribution model, window, type, and optional source filter; the
[Contacts](/data-warehouse/datasets/contacts) dataset asks how custom-field columns should be
named. Most datasets need only a column selection.
Select **Create export**. For recurring cadences the first run is scheduled; for one-time it runs
right away. Cometly writes the data to your destination and records each run in the export's run
history.
Each export run is recorded with its status and timing. Open an export to view its run history.
## Exports are immutable
Once created, an export's **name is the only thing you can change**. To alter the cadence, window,
destination, dataset, or columns, delete the export and create a new one. This keeps every run's
output faithful to the configuration that produced it — a run's columns and shape never drift
underneath your pipeline.
## File layout (S3 and GCS destinations)
Files are written to your bucket under a Hive-style partitioned path — the same layout for both S3
and GCS destinations:
```
//space_id=/exported_date=/run_id=/
├──
└── _SUCCESS
```
* **``** is the dataset's path segment — `touchpoints`, `raw_conversions`, `raw_hits`,
`attributed_conversions`, `contacts`, `companies`, or `_ad_analytics` (for example
`facebook_ad_analytics`). Each dataset page lists its segment.
* **`exported_date`** is the date the export ran (not the event date) and is sortable, so your
pipeline can always find the most recent export.
* **`run_id`** makes every run unique, so runs never overwrite each other and retries are safe.
* **`_SUCCESS`** is written last. Only ingest a run folder once its `_SUCCESS` marker is present, so
you never read a partially written run.
## Loading the data (S3 and GCS destinations)
Point your ingestion tooling — Snowflake, BigQuery, Databricks Auto Loader, Athena, and others — at
the `…//space_id=/` prefix to pick up new partitions automatically.
Each recurring run re-exports the **entire window**, not just new rows — so the most recent
`exported_date` is the current snapshot of that window. Deduplicate on a stable key (such as the
row's id, or the id plus metric date for ad analytics) when loading.
For a **Snowflake or BigQuery destination**, there's no file layout to manage — Cometly loads each
run directly into the target table you named at creation, appending rows (Snowflake) or running an
append-only load job (BigQuery). Cometly does not de-duplicate on your behalf — the same re-export
and deduplication behavior applies: each run re-loads the entire window, so rows repeat across runs
and you deduplicate on a stable key on your side. See
[De-duplicating loaded rows](/data-warehouse/bigquery-destination#de-duplicating-loaded-rows) for a
BigQuery example.
# Ad analytics
Source: https://docs.cometly.com/data-warehouse/datasets/ad-analytics
Daily ad-performance rows per channel — Facebook, Google, LinkedIn, TikTok, Microsoft, Reddit, Taboola, AdRoll, Whop, and X.
## What it is
Ad analytics is **one dataset per channel** — not a single unified dataset — because each channel's daily-performance table has a different shape (different metrics, different metric-date column). Each row is one day of performance for one ad-hierarchy entity.
* **Grain:** one row per entity per day — the `cometly_*_id` columns plus the metric date.
* **Windowed on:** the channel's **metric date** (a local calendar date — see below).
* **Path segment:** `_ad_analytics` (for example `facebook_ad_analytics`).
Each channel's columns are listed in the tabs below.
### Identity columns
Every channel exposes its ad hierarchy as IDs only (never names). Each level carries:
* `cometly__id` — **Locked.** Cometly's internal ID; part of the grain and the key for joining to [Raw touchpoints](/data-warehouse/datasets/raw-touchpoints) and [Attributed conversions](/data-warehouse/datasets/attributed-conversions).
* `platform__id` — **Optional.** The platform's own ID. Selecting it joins that level's entity table on demand, so the join cost is only paid when you want it. May be null.
The hierarchy levels differ by channel (for example LinkedIn uses campaign group → campaign → creative). Resolve names on your side from these IDs. See [Column conventions](/data-warehouse/overview#column-conventions).
### Dates and restatement
Unlike every other dataset, ad analytics windows on a **local calendar date**, not a UTC timestamp. Cometly matches your picked dates **literally** against the channel's metric date and makes **no timezone conversion** — the same behavior as the ads manager. Which timezone that date is in depends on the channel:
| Metric date timezone | Channels | Effect |
| - | - | - |
| **Space timezone** | Facebook, LinkedIn, TikTok, Reddit, Taboola, AdRoll, Whop | Picked dates match exactly. |
| **Ad-account timezone** | Google, Microsoft, X | Picked dates are approximate — the window can be off by up to a day at the edges. |
**Recent days may still change.** Ad platforms restate recent days' numbers. A recurring run
re-exports its full window with current values, so recent days converge across runs — but a day
restated *after* it leaves the window is not re-exported. For a full refresh, run a one-time export.
Hourly cadence is allowed (the tables refresh through the day, so hourly delivers the freshest
restatements) but its trailing window is capped at **7 days** for ad-analytics datasets, versus 14
for other datasets. Daily (up to 90 days) and one-time are unchanged.
## Columns by channel
Path segment `facebook_ad_analytics`. Metric date column `metric_date_local` (**Locked**, space timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Ad set | `cometly_adset_id` | `platform_adset_id` |
| Ad | `cometly_ad_id` | `platform_ad_id` |
**Performance** (all optional): `delivery`, `amount_spent`, `impressions`, `link_clicks`, `clicks`, `video_view`.
**Reported conversions** (all optional): `reported_purchases`, `reported_leads`, `reported_add_to_cart`, `reported_initiate_checkout`, `reported_complete_registration`, `reported_contact`, `reported_schedule`, `reported_start_trial`, `reported_subscribe`, `reported_view_content`, `reported_search`, `reported_add_payment_info`, `reported_add_to_wishlist`, `reported_landing_page_view`.
**Reported value** (all optional): `reported_revenue`, `reported_leads_value`, `reported_add_to_cart_value`, `reported_initiate_checkout_value`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
Path segment `google_ad_analytics`. Metric date column `metric_date_google` (**Locked**, ad-account timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Ad group | `cometly_adgroup_id` | `platform_adgroup_id` |
| Ad | `cometly_ad_id` | `platform_ad_id` |
**Performance** (all optional): `spend`, `clicks`, `impressions`, `ctr`, `interactions`, `interaction_rate`, `video_views`, `video_view_rate`.
**Averages** (all optional): `average_cpm`, `average_cpc`, `average_cost`, `average_cpe`, `average_cpv`.
**Conversions** (all optional): `conversions`, `all_conversions`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
Path segment `linkedin_ad_analytics`. Metric date column `metric_date` (**Locked**, space timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign group | `cometly_campaign_group_id` | `platform_campaign_group_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Creative | `cometly_creative_id` | `platform_creative_id` |
**Performance** (all optional): `cost`, `clicks`, `impressions`, `video_views`.
**Conversions & leads** (all optional): `conversions`, `conversion_value`, `leads`, `registrations`, `one_click_lead_form_opens`, `form_completion_rate`, `external_website_post_view_conversions`, `external_website_post_click_conversions`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
Path segment `tiktok_ad_analytics`. Metric date column `metric_date_local` (**Locked**, space timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Ad group | `cometly_adgroup_id` | `platform_adgroup_id` |
| Ad | `cometly_ad_id` | `platform_ad_id` |
**Performance** (all optional): `amount_spent`, `impressions`, `clicks`.
**Reported conversions** (all optional): `reported_purchases`, `reported_revenue`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
Path segment `microsoft_ad_analytics`. Metric date column `metric_date_microsoft` (**Locked**, ad-account timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Ad group | `cometly_adgroup_id` | `platform_adgroup_id` |
| Ad | `cometly_ad_id` | `platform_ad_id` |
**Performance** (all optional): `spend`, `clicks`, `impressions`, `video_views`.
**Conversions** (all optional): `conversions_qualified`, `view_through_conversions_qualified`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
Path segment `reddit_ad_analytics`. Metric date column `metric_date_reddit` (**Locked**, space timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Ad group | `cometly_adgroup_id` | `platform_adgroup_id` |
| Ad | `cometly_ad_id` | `platform_ad_id` |
**Performance** (all optional): `spend`, `clicks`, `impressions`, `reddit_video_views`.
**Conversions** (all optional): `reddit_reported_roas`, `conversion_purchase_total_value`, `conversion_page_visit_view`, `conversion_sign_up_views`, `conversion_lead_views`, `conversion_purchase_views`, `conversion_page_visit_clicks`, `conversion_sign_up_clicks`, `conversion_lead_clicks`, `conversion_purchase_clicks`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
Path segment `taboola_ad_analytics`. Metric date column `metric_date_taboola` (**Locked**, space timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Ad group | `cometly_adgroup_id` | `platform_adgroup_id` |
| Ad | `cometly_ad_id` | `platform_ad_id` |
Campaign, ad-group, and ad IDs can be null on Taboola rows, so their platform IDs may come back null.
**Performance** (all optional): `spend`, `clicks`, `impressions`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
Path segment `adroll_ad_analytics`. Metric date column `metric_date_adroll` (**Locked**, space timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Ad group | `cometly_adgroup_id` | `platform_adgroup_id` |
| Ad | `cometly_ad_id` | `platform_ad_id` |
Campaign, ad-group, and ad IDs can be null on AdRoll rows, so their platform IDs may come back null.
**Performance** (all optional): `spend`, `clicks`, `impressions`.
**Conversions** (all optional): `conversions`, `click_throughs`, `view_throughs`, `revenue`, `click_revenue`, `view_revenue`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
Path segment `whop_ad_analytics`. Metric date column `metric_date_whop` (**Locked**, space timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Ad group | `cometly_adgroup_id` | `platform_adgroup_id` |
| Ad | `cometly_ad_id` | `platform_ad_id` |
A Whop "account" is a Whop company (`biz_xxx`); that id is the `platform_account_id` and also doubles as the `company_id` required by Whop's Conversions API.
**Performance** (all optional): `spend`, `clicks`, `impressions`, `reach`.
**Results** (all optional): `whop_result_count`, `whop_result_label_key`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
Path segment `x_ad_analytics`. Metric date column `metric_date_x` (**Locked**, ad-account timezone).
**Identity**
| Level | Cometly ID (Locked) | Platform ID (Optional) |
| - | - | - |
| Account | `cometly_account_id` | `platform_account_id` |
| Campaign | `cometly_campaign_id` | `platform_campaign_id` |
| Ad group | `cometly_adgroup_id` | `platform_adgroup_id` |
| Ad | `cometly_ad_id` | `platform_ad_id` |
**Performance** (all optional): `spend`, `clicks`, `x_url_clicks`, `impressions`, `x_video_views`, `x_engagements`.
**Conversions** (all optional): `conversion_purchase_total_value`, `conversion_purchases`, `conversion_sign_ups`, `conversion_site_visits`, `conversion_downloads`, `conversion_custom`.
**Timestamps** (all optional): `created_at`, `updated_at` (last restated).
# Attributed conversions
Source: https://docs.cometly.com/data-warehouse/datasets/attributed-conversions
Conversions joined to their attributed touchpoints, at the attributed-touchpoint grain.
## What it is
The **Attributed conversions** dataset runs your conversions through the same attribution engine the report builder uses, and unloads the un-aggregated result: each row is a conversion joined to a touchpoint that earned credit toward it.
* **Grain:** the **attribution model decides**. Single-touch models (first/last/platform/non-direct) emit exactly the winning touchpoint — **one row per conversion**. Multi-touch models (linear, u-shaped) emit **one row per touchpoint** with its fractional credit, so a conversion's rows sum to its full value.
* **Windowed on:** the conversion's `event_time_utc` (UTC timestamp).
* **Path segment:** `attributed_conversions`.
**Two independent windows apply.** The export window bounds which **conversions** are included. The
separate **attribution window** (a setting below) bounds how far back a **touchpoint** may sit
before its conversion to still earn credit.
## Settings
Configured once at creation (immutable, like the rest of the export):
`first_touch`, `last_touch`, `first_platform_touch`, `last_platform_touch`, `last_non_direct_touch`,
`linear`, `linear_paid`, or `u_shaped`. Determines the row grain (see above).
How far back a touchpoint may sit before its conversion: `1`, `7`, `14`, `28`, `30`, `60`, or `90`
days, or **LTV** (no limit).
Attribute by **profile** (the contact) or by **company**.
Optional source filter. Empty means **all sources** (the report-builder default).
## Columns
### Attributed conversion fields
| Column | Status | Description |
| - | - | - |
| `conversion_id` | Locked | The conversion's ID. |
| `touchpoint_id` | Locked | The attributed touchpoint's ID. |
| `touchpoint_count_contribution` | Locked | The fractional conversion **count** credited to this touchpoint. |
| `touchpoint_amount_contribution` | Locked | The fractional conversion **value** credited to this touchpoint. |
| `event_time_utc` | Locked | When the conversion occurred (UTC). |
| `event_name` | Optional | The raw conversion event name. |
| `configured_name` | Optional | The event's display name as configured for your space (falls back to the raw name). |
| `amount` | Optional | Full monetary value of the conversion. |
| `profile_id` | Optional | The [contact](/data-warehouse/datasets/contacts) the conversion belongs to. |
| `company_id` | Optional | The [company](/data-warehouse/datasets/companies) the contact belongs to — joins to the Companies dataset's `id`. From there, join to your own account tables on `companies.external_id`, which is your `company_external_id` as stored: surrounding whitespace trimmed, case preserved, and integer values stored as their decimal string (`1234567`). The match is byte-exact and case-sensitive, so join on exact equality, not a case-insensitive one. |
| `source` | Optional | Traffic source of the attributed touchpoint. |
| `cometly_account_id` | Optional | Cometly's internal ad-account ID. |
| `platform_account_id` | Optional | The platform's own ad-account ID. |
| `cometly_campaign_id` | Optional | Cometly's internal campaign ID. |
| `platform_campaign_id` | Optional | The platform's own campaign ID. |
| `cometly_adset_id` | Optional | Cometly's internal ad set ID. |
| `platform_adset_id` | Optional | The platform's own ad set ID. |
| `cometly_ad_id` | Optional | Cometly's internal ad ID. |
| `platform_ad_id` | Optional | The platform's own ad ID. |
### Conversion metadata
The visit-context record for the **conversion**. Each column is prefixed `conversion_`. All optional.
| Column | Status | Description |
| - | - | - |
| `conversion_order_id` | Optional | Order ID from the conversion event. |
| `conversion_order_name` | Optional | Order name or number. |
| `conversion_idempotency_key` | Optional | Idempotency key of the event. |
| `conversion_event_url` | Optional | Full event URL, assembled from host, path, and query. |
| `conversion_referrer_url` | Optional | Full referrer URL. |
| `conversion_fbclid` | Optional | Facebook click ID. |
| `conversion_fbp` | Optional | Facebook browser ID (fbp). |
| `conversion_gclid` | Optional | Google click ID. |
| `conversion_ttclid` | Optional | TikTok click ID. |
| `conversion_lnclid` | Optional | LinkedIn click ID. |
| `conversion_msclkid` | Optional | Microsoft click ID. |
| `conversion_rdt_cid` | Optional | Reddit click ID. |
| `conversion_tblci` | Optional | Taboola click ID. |
| `conversion_device_type` | Optional | Device type. |
| `conversion_os` | Optional | Operating system. |
| `conversion_browser` | Optional | Browser. |
| `conversion_language` | Optional | Browser language. |
### Touchpoint metadata
The visit-context record for the **attributed touchpoint** (the ad-click visit). Each column is prefixed `touchpoint_`. All optional.
| Column | Status | Description |
| - | - | - |
| `touchpoint_order_id` | Optional | Order ID from the touchpoint event. |
| `touchpoint_order_name` | Optional | Order name or number. |
| `touchpoint_idempotency_key` | Optional | Idempotency key of the event. |
| `touchpoint_event_url` | Optional | Full event URL, assembled from host, path, and query. |
| `touchpoint_referrer_url` | Optional | Full referrer URL. |
| `touchpoint_fbclid` | Optional | Facebook click ID. |
| `touchpoint_fbp` | Optional | Facebook browser ID (fbp). |
| `touchpoint_gclid` | Optional | Google click ID. |
| `touchpoint_ttclid` | Optional | TikTok click ID. |
| `touchpoint_lnclid` | Optional | LinkedIn click ID. |
| `touchpoint_msclkid` | Optional | Microsoft click ID. |
| `touchpoint_rdt_cid` | Optional | Reddit click ID. |
| `touchpoint_tblci` | Optional | Taboola click ID. |
| `touchpoint_device_type` | Optional | Device type. |
| `touchpoint_os` | Optional | Operating system. |
| `touchpoint_browser` | Optional | Browser. |
| `touchpoint_language` | Optional | Browser language. |
For a multi-touch conversion, the **conversion** metadata repeats across that conversion's
touchpoint rows, while the **touchpoint** metadata differs per row.
# Companies
Source: https://docs.cometly.com/data-warehouse/datasets/companies
Every company created in the export window.
## What it is
The **Companies** dataset is one row per company created in your space within the window. It's the simplest dataset — one table, **no joins**.
* **Grain:** one row per company (`id`).
* **Windowed on:** `created_at` (UTC timestamp) — the company's creation time.
* **Path segment:** `companies`.
A company's identity within a space is its numeric **`id`**. A company may carry a **`domain`**, an **`external_id`** (your own company identifier), or both — whichever identifiers were used to match events to it. `domain` is `null` for a company created from an external ID alone, with no associated email domain. The `id` is also locked because it's what the [Contacts](/data-warehouse/datasets/contacts) dataset's `primary_company_id` references — it's the key for joining the two exports in your warehouse.
Which identifier a company carries depends on your Space's company identity mode (Space Settings → Additional Setup → Company Tracking): in the default **Auto** mode, companies are grouped by email domain, so rows carry `domain`; in **External ID** mode, companies are grouped by your own `company_external_id` alone, so rows carry `external_id`. A company carries both only if you explicitly set both — for example via [Update Company](/api-reference/endpoint/update-company), or by switching identity modes over the company's lifetime.
Like [Contacts](/data-warehouse/datasets/contacts), companies are mutable. Each run carries a
company's state **as of run time**, but a company created *before* the window is not re-exported
when it later changes. For a full refresh, run a one-time export over all history.
A company that later gains an `external_id` (or a domain) is not re-exported by an incremental run —
incremental runs are windowed on `created_at`, not on when the company last changed. Run a one-time
export over all history to pick up identifiers assigned after a company's creation window. In
particular, if you **enable External ID mode on an existing Space**, run a one-time export over all
history so that companies which gain an `external_id` after their original creation window are
picked up.
## Columns
### Company fields
| Column | Status | Description |
| - | - | - |
| `id` | Locked | Company ID — the join key referenced by the Contacts dataset's `primary_company_id`. |
| `created_at` | Locked | When the company was created (UTC). |
| `domain` | Optional (nullable) | The company's email domain, if it has one. `null` for a company identified only by `external_id`. |
| `external_id` | Optional (nullable) | The customer's own identifier for this company, if one was supplied. |
| `name` | Optional | Display name. For a company created from an external ID alone, this defaults to the supplied company name, falling back to the external ID itself. |
| `updated_at` | Optional | When the company was last updated (UTC). |
# Contacts
Source: https://docs.cometly.com/data-warehouse/datasets/contacts
Every contact created in the export window, with its identity and custom fields.
## What it is
The **Contacts** dataset is one row per contact created in your space within the window. A *contact* is a person known to your tracker (internally called a profile).
* **Grain:** one row per contact (`id`).
* **Windowed on:** `created_at` (UTC timestamp) — the contact's **original creation** time. The internal row timestamp is never exported.
* **Path segment:** `contacts`.
Contacts are mutable. Each run carries a contact's state **as of run time**, so re-runs self-heal
updates for contacts created *inside* the window. A contact created *before* the window is not
re-exported when it later changes — for a full refresh, run a one-time export over all history.
Contacts flagged internally as pathological (runaway identity graphs) are excluded outright.
## Columns
### Contact
| Column | Status | Description |
| - | - | - |
| `id` | Locked | Contact ID — the row grain and dedup key. |
| `created_at` | Locked | The contact's original creation time (UTC). |
### Identity
| Column | Status | Description |
| - | - | - |
| `name` | Optional | Contact name. |
| `email` | Optional | Primary email. |
| `phone` | Optional | Primary phone. |
| `location` | Optional | Location label. |
### Location & device
| Column | Status | Description |
| - | - | - |
| `city` | Optional | City. |
| `state` | Optional | State or region. |
| `country` | Optional | Country. |
| `device_type` | Optional | Most recent device type. |
| `os` | Optional | Most recent operating system. |
| `browser` | Optional | Most recent browser. |
| `language` | Optional | Most recent browser language. |
### Timestamps
| Column | Status | Description |
| - | - | - |
| `updated_at` | Optional | When the contact was last updated (UTC). |
### Company
| Column | Status | Description |
| - | - | - |
| `primary_company_id` | Optional | The contact's primary [company](/data-warehouse/datasets/companies) ID — joins to the Companies dataset's `id`. From there, join to your own account tables on `companies.external_id`, which is your `company_external_id` as stored: surrounding whitespace trimmed, case preserved, and integer values stored as their decimal string (`1234567`). The match is byte-exact and case-sensitive, so join on exact equality, not a case-insensitive one. |
### Custom fields
Your space's custom contact fields, by slot. The slot number determines its type:
| Slots | Type |
| - | - |
| `profile_field_1` – `profile_field_15` | Text |
| `profile_field_16` – `profile_field_25` | Number |
| `profile_field_26` – `profile_field_30` | Date |
All optional. The create form shows only the slots your space has configured, labeled with each field's name.
**Output naming is your choice at creation.** Custom-field columns can be named by their **stable
slot** (`profile_field_7`) or by the field's **label** (frozen as of creation). With label naming,
renaming a field later does not change an existing export's columns.
### Identity graph
The contact's full multi-value identity, exported as **JSON arrays**. All optional and default to off. Values inherited from contacts merged into this one are included, and the lists are **uncapped** (unlike the Public API's recent-5 token list).
| Column | Status | Description |
| - | - | - |
| `emails` | Optional | JSON array of all the contact's emails. |
| `phones` | Optional | JSON array of all the contact's phones. |
| `comet_tokens` | Optional | JSON array of all the contact's Comet tokens (browser identifiers). |
| `tracking_ids` | Optional | JSON array of every `tracking_id` ever sent on this contact's events (Create Event API `tracking_id`, webhook `tracking_id`) — your own application/customer key back to your systems. |
| `merged_profile_ids` | Optional | JSON array of the contact IDs merged into this contact — your repair kit for event rows exported before a merge. |
# Raw conversions
Source: https://docs.cometly.com/data-warehouse/datasets/raw-conversions
Every conversion as recorded — the unattributed counterpart to touchpoints.
## What it is
The **Raw conversions** dataset is one row per conversion recorded for your space within the window. It is the raw record with no attribution applied — to join conversions to the touchpoints that earned credit, use [Attributed conversions](/data-warehouse/datasets/attributed-conversions).
* **Grain:** one row per conversion (`id`).
* **Windowed on:** `event_time_utc` (UTC timestamp).
* **Path segment:** `raw_conversions`.
Selecting any **Visit metadata** column joins the conversion's visit-context record; otherwise no join is performed.
## Columns
### Conversion fields
| Column | Status | Description |
| - | - | - |
| `id` | Locked | Conversion ID — the row grain and dedup key. |
| `event_time_utc` | Locked | When the conversion occurred (UTC). |
| `event_name` | Optional | The raw event name as tracked. |
| `configured_name` | Optional | The event's display name as configured for your space (falls back to the raw event name). |
| `amount` | Optional | Monetary value of the conversion. |
| `profile_id` | Optional | The [contact](/data-warehouse/datasets/contacts) the conversion belongs to. |
| `company_id` | Optional | The [company](/data-warehouse/datasets/companies) the contact belongs to — joins to the Companies dataset's `id`. From there, join to your own account tables on `companies.external_id`, which is your `company_external_id` as stored: surrounding whitespace trimmed, case preserved, and integer values stored as their decimal string (`1234567`). The match is byte-exact and case-sensitive, so join on exact equality, not a case-insensitive one. |
| `is_upsell` | Optional | Whether the conversion is flagged as an upsell. |
### Visit metadata
The visit-context record for the conversion. All optional; selecting any one joins the metadata table.
| Column | Status | Description |
| - | - | - |
| `order_id` | Optional | Order ID from the source event, if any. |
| `order_name` | Optional | Order name or number from the source event. |
| `idempotency_key` | Optional | Idempotency key of the source event. |
| `event_url` | Optional | Full event URL, assembled from host, path, and query string. |
| `referrer_url` | Optional | Full referrer URL, assembled from referrer host, path, and query. |
| `fbclid` | Optional | Facebook click ID captured on the visit. |
| `fbp` | Optional | Facebook browser ID (fbp). |
| `gclid` | Optional | Google click ID. |
| `ttclid` | Optional | TikTok click ID. |
| `lnclid` | Optional | LinkedIn click ID. |
| `msclkid` | Optional | Microsoft click ID. |
| `rdt_cid` | Optional | Reddit click ID. |
| `tblci` | Optional | Taboola click ID. |
| `device_type` | Optional | Device type of the visit. |
| `os` | Optional | Operating system. |
| `browser` | Optional | Browser. |
| `language` | Optional | Browser language. |
# Raw hits
Source: https://docs.cometly.com/data-warehouse/datasets/raw-hits
Every tracked pixel hit as recorded, unfiltered across all event types.
## What it is
The **Raw hits** dataset is the rawest dump: one row per tracking-pixel hit recorded for your space within the window, **unfiltered** across all event types (page views and everything else). It comes straight off the hits table with **no joins**.
* **Grain:** one row per hit (`id`).
* **Windowed on:** `event_time_utc` (UTC timestamp).
* **Path segment:** `raw_hits`.
Two things differ from the other raw datasets:
* **Host, path, and query are separate columns** (not combined into a single `event_url`), because path-level grouping is the primary use of page-view data.
* **`event_name` is locked on** — an unfiltered, all-event-types dump would be ambiguous without it.
## Columns
### Hit fields
| Column | Status | Description |
| - | - | - |
| `id` | Locked | Hit ID — the row grain and dedup key. |
| `event_time_utc` | Locked | When the hit fired (UTC). |
| `event_name` | Locked | The hit's event type (for example `page_view`). |
| `source` | Optional | Traffic source of the hit. |
| `profile_id` | Optional | The [contact](/data-warehouse/datasets/contacts) the hit is currently attributed to. |
| `original_profile_id` | Optional | The contact the hit was first attributed to, before any later identity merges. |
### Page
| Column | Status | Description |
| - | - | - |
| `host` | Optional | Host of the hit URL. |
| `path` | Optional | Path of the hit URL. |
| `query` | Optional | Query string of the hit URL. |
| `referrer_host` | Optional | Host of the referrer URL. |
### Device & location
| Column | Status | Description |
| - | - | - |
| `country` | Optional | Country of the visit. |
| `device_type` | Optional | Device type. |
| `browser` | Optional | Browser. |
| `os` | Optional | Operating system. |
### Identifiers
These columns carry sensitive visitor identifiers. They default to off — select them only if your
warehouse handling and retention policies allow it.
| Column | Status | Description |
| - | - | - |
| `ip` | Optional | IP address of the visit. |
| `web_fingerprint` | Optional | Browser fingerprint. |
| `comet_token` | Optional | The Comet token (browser identifier) for the visit. |
# Raw touchpoints
Source: https://docs.cometly.com/data-warehouse/datasets/raw-touchpoints
Every tracked touchpoint as recorded, over the export window.
## What it is
The **Raw touchpoints** dataset is one row per touchpoint recorded for your space within the window. It is the raw, unattributed record — for attribution credit per touchpoint, use [Attributed conversions](/data-warehouse/datasets/attributed-conversions) instead.
* **Grain:** one row per touchpoint (`id`).
* **Windowed on:** `event_time_utc` (UTC timestamp).
* **Path segment:** `touchpoints`.
Selecting any **Visit metadata** column joins the touchpoint's visit-context record; otherwise no join is performed. Ad entities are carried as IDs only — see [Column conventions](/data-warehouse/overview#column-conventions).
## Columns
### Touchpoint fields
| Column | Status | Description |
| - | - | - |
| `id` | Locked | Touchpoint ID — the row grain and dedup key. |
| `event_time_utc` | Locked | When the touchpoint occurred (UTC). |
| `profile_id` | Optional | The [contact](/data-warehouse/datasets/contacts) the touchpoint belongs to. |
| `company_id` | Optional | The [company](/data-warehouse/datasets/companies) the contact belongs to — joins to the Companies dataset's `id`. From there, join to your own account tables on `companies.external_id`, which is your `company_external_id` as stored: surrounding whitespace trimmed, case preserved, and integer values stored as their decimal string (`1234567`). The match is byte-exact and case-sensitive, so join on exact equality, not a case-insensitive one. |
| `source` | Optional | Traffic source of the touchpoint (for example `facebook_ads`). |
| `cometly_account_id` | Optional | Cometly's internal ad-account ID. |
| `platform_account_id` | Optional | The platform's own ad-account ID. |
| `cometly_campaign_id` | Optional | Cometly's internal campaign ID. |
| `platform_campaign_id` | Optional | The platform's own campaign ID. |
| `cometly_adset_id` | Optional | Cometly's internal ad set ID. |
| `platform_adset_id` | Optional | The platform's own ad set ID. |
| `cometly_ad_id` | Optional | Cometly's internal ad ID. |
| `platform_ad_id` | Optional | The platform's own ad ID. |
### Visit metadata
The visit-context record for the touchpoint. All optional; selecting any one joins the metadata table.
| Column | Status | Description |
| - | - | - |
| `order_id` | Optional | Order ID from the source event, if any. |
| `order_name` | Optional | Order name or number from the source event. |
| `idempotency_key` | Optional | Idempotency key of the source event. |
| `event_url` | Optional | Full event URL, assembled from host, path, and query string. |
| `referrer_url` | Optional | Full referrer URL, assembled from referrer host, path, and query. |
| `fbclid` | Optional | Facebook click ID captured on the visit. |
| `fbp` | Optional | Facebook browser ID (fbp). |
| `gclid` | Optional | Google click ID. |
| `ttclid` | Optional | TikTok click ID. |
| `lnclid` | Optional | LinkedIn click ID. |
| `msclkid` | Optional | Microsoft click ID. |
| `rdt_cid` | Optional | Reddit click ID. |
| `tblci` | Optional | Taboola click ID. |
| `device_type` | Optional | Device type of the visit. |
| `os` | Optional | Operating system. |
| `browser` | Optional | Browser. |
| `language` | Optional | Browser language. |
The `cometly_*_id` columns are the stable keys for joining touchpoints to the
[Ad analytics](/data-warehouse/datasets/ad-analytics) datasets in your warehouse.
# Set up a Google Cloud Storage destination
Source: https://docs.cometly.com/data-warehouse/gcs-destination
Create a GCS bucket, service account, and HMAC key so Cometly can write export files to your bucket.
## Overview
A **GCS destination** is a reusable connection to a Google Cloud Storage bucket you own. Like an S3 destination, Cometly writes Parquet files to your bucket and stops — there's no load phase. You load the files into BigQuery, Databricks, or another engine yourself, on your own schedule.
Cometly authenticates with an **HMAC interoperability key**, not a service-account JSON key. You generate one for a service account that has write access to your bucket.
## Prerequisites
* A Google Cloud project with permission to create Cloud Storage buckets, service accounts, and IAM bindings.
* A space administrator role in Cometly.
## Create the bucket and credentials
In the Google Cloud Console, create a Cloud Storage bucket (or choose an existing one) for Cometly
exports, and note its **name**.
Create a dedicated service account (for example, `cometly-exports`) for programmatic access only.
The export only ever **lists the bucket's prefix and writes objects** — it never deletes anything.
Create a [custom role](https://cloud.google.com/iam/docs/creating-custom-roles) with exactly these
permissions:
* `storage.objects.list` — the export lists its output prefix
* `storage.objects.create` — the export writes the Parquet files
* `storage.multipartUploads.create`, `storage.multipartUploads.abort`, and
`storage.multipartUploads.listParts` — large files upload in parts through the S3-compatible API
Grant the role to the service account **on the bucket** (Bucket → Permissions → Grant access), not
on the project.
Because the role excludes `storage.objects.delete`, the connection check on save can't clean up its
tiny `.cometly-connection-test/…` probe object — that's expected; remove it manually if you like.
In Cloud Storage, go to **Settings → Interoperability**, and create an **HMAC key** for the
service account. Save the **Access key** and **Secret** — the secret is shown only once.
In the dashboard, go to **Integrations → Google Cloud Storage → Add destination** and enter:
A label for this destination, for example "Production warehouse".
The GCS bucket name from Step 1.
The access key from the interoperability key you generated, for example `GOOG1E…`.
The secret from the interoperability key. Shown only once when you generate it in Cloud Storage.
Cometly runs a quick, best-effort connection check on save. Because permissions scoped to a prefix
can legitimately deny a whole-bucket check, a failed check is logged but never blocks the create —
if your key and bucket are correct, the destination still works even if the check couldn't confirm it.
Your HMAC secret is encrypted at rest and never shown again. When editing a destination, leave the
secret blank to keep the existing key.
Scope the service account to only the bucket used for exports. Avoid reusing a broadly privileged
service account.
## File layout
A GCS destination writes the same Hive-style partitioned Parquet layout as an S3 destination — see
[File layout (S3 and GCS destinations)](/data-warehouse/create-export#file-layout-s3-and-gcs-destinations)
for the path structure, partition columns, and the `_SUCCESS` marker.
## Using with BigQuery
BigQuery can read these files directly from your bucket, without a load step:
* Create an [external table](https://cloud.google.com/bigquery/docs/external-data-cloud-storage) over
the `…//space_id=/` prefix to query the Parquet files in place.
* Or run a load job to copy the data into a native BigQuery table — Cloud Storage load jobs are free.
Prefer a native load without managing external tables or load jobs yourself? Use a
[BigQuery destination](/data-warehouse/bigquery-destination) instead — Cometly loads directly into a
table in your dataset, the same way the Snowflake destination does, with no file layout to manage.
## Next step
Choose this destination, a dataset, and a date range.
# Data Warehouse Exports
Source: https://docs.cometly.com/data-warehouse/overview
Export your raw Cometly data as Parquet files to your own S3 or GCS bucket for loading into a data warehouse or lake.
## Overview
Data Warehouse Exports write your raw Cometly data as **Parquet files** to an **object storage bucket you own** — Amazon S3 or Google Cloud Storage — or load it directly into a warehouse table — Snowflake or BigQuery. Point your warehouse or lake tooling — Snowflake, BigQuery, Databricks, Athena, DuckDB, and others — at the bucket to load the data on your own schedule, or let Cometly load it for you.
You keep the raw, unaggregated data and model it however you like on your side.
This is different from the **API exports** in the API Reference
([Create Event Export](/api-reference/endpoint/create-event-export),
[Create Contact Export](/api-reference/endpoint/create-contact-export),
[Create Company Export](/api-reference/endpoint/create-company-export)). Those return gzipped NDJSON
for ad-hoc download through the API. Data Warehouse Exports deliver columnar Parquet files to your
own object storage for bulk, recurring loads. See
[Exports vs Pagination](/concepts/exports-vs-pagination) for the API-side options.
## How it works
Either a bucket Cometly writes Parquet files to — S3
([Set up an S3 destination](/data-warehouse/s3-destination)) or GCS
([Set up a Google Cloud Storage destination](/data-warehouse/gcs-destination)) — or a warehouse
Cometly loads into directly — Snowflake
([Set up a Snowflake destination](/data-warehouse/snowflake-destination)) or BigQuery
([Set up a BigQuery destination](/data-warehouse/bigquery-destination)).
Choose a destination, a dataset, a cadence, and a window. See [Create an export](/data-warehouse/create-export).
For S3 or GCS, Cometly writes Parquet files under your chosen path and you point your warehouse or
lake at the bucket. For Snowflake or BigQuery, Cometly loads each run into a table in your database.
## Datasets
Each export unloads exactly **one** dataset. To export several datasets, create several exports — each with its own cadence, window, destination, and columns.
| Dataset | Description |
| - | - |
| [Raw touchpoints](/data-warehouse/datasets/raw-touchpoints) | Every tracked touchpoint as recorded, over the window. |
| [Raw conversions](/data-warehouse/datasets/raw-conversions) | Every conversion as recorded — the unattributed counterpart to touchpoints. |
| [Raw hits](/data-warehouse/datasets/raw-hits) | Every tracked pixel hit as recorded, unfiltered across all event types. |
| [Attributed conversions](/data-warehouse/datasets/attributed-conversions) | Conversions joined to their attributed touchpoints, at the attributed-touchpoint grain. |
| [Contacts](/data-warehouse/datasets/contacts) | Every contact created in the window, with its identity and custom fields. |
| [Companies](/data-warehouse/datasets/companies) | Every company created in the window. |
| [Ad analytics](/data-warehouse/datasets/ad-analytics) | Daily ad-performance rows per channel (Facebook, Google, LinkedIn, TikTok, Microsoft, Reddit, Taboola, AdRoll, Whop, X). |
## Cadence and windows
An export runs on a **cadence** and covers a **window** of data. The two are independent: cadence is *how often* a run fires; the window is *how much* each run pulls.
| Cadence | Runs | Window |
| - | - | - |
| **One-time** | Once, then stops | An absolute start/end date range you pick. |
| **Daily** | Once a day, at an hour you choose | A trailing window (e.g. last 7 days), capped at **90 days**. |
| **Hourly** | Every hour | A trailing window, capped at **14 days** (7 days for ad-analytics datasets). |
A **trailing window** is anchored to "now" at run time — "last 7 days" means the 7 days ending at the moment the run fires, not a fixed calendar range. Every recurring run re-exports the **full** window (not just what changed), so runs are self-healing and you deduplicate on a stable key on your side.
A run only re-exports rows that are still **inside** the window. To refresh data older than the
window (for example, after a backfill or a late restatement), create a **one-time** export over the
date range you want. The common pattern is a one-time backfill plus a recurring incremental export.
## Column conventions
These conventions hold across every dataset, so the same column name means the same thing in every export.
* **Timestamps are UTC.** Every `*_utc` column (and `created_at` / `updated_at`) is a UTC timestamp. The one exception is the ad-analytics **metric date**, which is a local calendar date — see that dataset's page.
* **Locked vs optional columns.** Some columns are **locked** (always exported — the ones that keep the data coherent, such as row identity and join keys); the rest are **optional** and you pick them at creation. Each dataset page marks which is which.
* **Cometly ID vs platform ID.** Ad entities (account, campaign, ad set/ad group, ad) are carried as **IDs only — never names**. Each level exposes two columns:
* `cometly__id` — Cometly's own internal ID for the entity. Stable, and the reliable key for joining one export to another (e.g. touchpoints to ad analytics).
* `platform__id` — the advertising platform's own ID for the same entity (the Facebook/Google/etc. ID). The key you join to your platform-side data; may be null.
Resolve campaign/ad **names** on your side from these IDs.
* **JSON-array columns.** A few columns (the contacts identity-graph columns) are exported as JSON-encoded arrays. They're optional and default to off.
## Where to manage exports
In the Cometly dashboard, each destination type is its own integration under the **Warehouse** category on the Integrations page: go to **Integrations → Amazon S3**, **Integrations → Google Cloud Storage**, **Integrations → Snowflake**, or **Integrations → BigQuery**, depending on the destination type. Each page has a **Destinations** section scoped to that type, and an **Exports** table that lists every export across all destination types — so you can manage all your exports from any of the four pages. This feature is available to space administrators.
# Set up an S3 destination
Source: https://docs.cometly.com/data-warehouse/s3-destination
Create an AWS IAM user and S3 bucket so Cometly can write export files to your account.
## Overview
A **destination** is a reusable connection to an S3 bucket you own. You configure it once, then point any number of exports at it. Cometly authenticates with an **IAM access key** that has write access to your bucket.
## Prerequisites
* An AWS account with permission to create S3 buckets and IAM users and policies.
* A space administrator role in Cometly.
## Create the bucket and credentials
In the AWS S3 console, create a bucket (or choose an existing one) for Cometly exports, and note
its **name** and **region** (for example, `us-east-1`).
In the AWS IAM console, create a policy that grants write access to that bucket. Replace
`YOUR_BUCKET_NAME` with your bucket name.
```json theme={null}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::YOUR_BUCKET_NAME",
"arn:aws:s3:::YOUR_BUCKET_NAME/*"
]
}
]
}
```
Create a dedicated IAM user (for example, `cometly-exports`) for programmatic access only, and
attach the policy from the previous step.
For that user, create an access key. Save the **Access Key ID** and **Secret Access Key** — the
secret is shown only once.
In the dashboard, go to **Integrations → Amazon S3 → Add destination** and enter:
A label for this destination, for example "Production warehouse".
The S3 bucket name from Step 1.
The bucket's AWS region.
The Access Key ID from Step 4.
The Secret Access Key from Step 4.
Your Secret Access Key is encrypted at rest and never shown again. When editing a destination,
leave the secret blank to keep the existing key.
Grant the IAM user access only to the bucket used for exports. Avoid reusing a broadly privileged
access key.
## Next step
Choose this destination, a dataset, and a date range.
# Set up a Snowflake destination
Source: https://docs.cometly.com/data-warehouse/snowflake-destination
Create a Snowflake role, service user, and access token so Cometly can load export data directly into your warehouse.
## Overview
A **Snowflake destination** is a reusable connection to a database and schema you own. Unlike an S3 or GCS destination — where Cometly writes Parquet files and you load them yourself — a Snowflake destination has Cometly **load the data directly into a table in your warehouse** on each run. Your warehouse's own compute does the load.
You give Cometly a role that can create and append to a table in one schema, and a token to authenticate with. Cometly handles the rest.
## Prerequisites
* A Snowflake account, and a role that can create warehouses, databases, roles, users, and network policies (typically `ACCOUNTADMIN`).
* A space administrator role in Cometly.
## Connect Cometly to Snowflake
In Snowflake (as a role such as `ACCOUNTADMIN`), set up:
* A **warehouse** for Cometly to run the load on.
* A **database and schema** to hold the exported tables.
* A **role** that can `CREATE TABLE` in that schema (grant it `USAGE` on the warehouse, database, and schema, plus `CREATE TABLE` on the schema).
* A **network policy** — required for a service user's token to work. Restrict its allowed IPs for production use.
* A **service user** (type `SERVICE`), granted the role above and governed by that network policy.
* A **programmatic access token** on that service user, restricted to the role. Copy the token secret — it is shown only once.
```sql theme={null}
SELECT CURRENT_ORGANIZATION_NAME() AS org, CURRENT_ACCOUNT_NAME() AS account;
```
Your account identifier is `org-account` (with a hyphen) — for example, `myorg-ab12345`. Use that,
**not** the full `https://…snowflakecomputing.com` URL.
In the dashboard, go to **Integrations → Snowflake → Add destination** and enter:
A label for this destination, for example "Production warehouse".
Your `org-account` identifier from the query above.
The warehouse Cometly runs the load on, for example `cometly_wh`.
The database that holds the target schema, for example `cometly_db`.
The schema Cometly creates the table in, for example `exports`.
The role Cometly uses — it must be able to `CREATE TABLE` in the schema, for example `cometly_role`.
The programmatic access token secret from the setup script.
Cometly runs a quick connection check on save. If it succeeds, the destination is connected.
The access token is encrypted at rest and never shown again. Programmatic access tokens **expire**
(`DAYS_TO_EXPIRY`) — rotate the token in Snowflake and update the destination before then, or the
export will start failing.
A service user's token only works when the user is governed by a **network policy** (created in the
setup script above). Without one, Snowflake rejects the token. Restrict the policy's `ALLOWED_IP_LIST`
for production use.
Cometly creates each export's table **as the role above**, so that role owns it. To let your own
reporting roles read the exported tables, grant them access — a one-time **future grant** covers
every table Cometly creates:
```sql theme={null}
GRANT SELECT ON FUTURE TABLES IN SCHEMA cometly_db.exports TO ROLE ;
```
## Next step
Choose this destination, a dataset, a target table name, and a date range.
# Account-level attribution for B2B (company external ID)
Source: https://docs.cometly.com/guides/b2b-account-attribution
Attribute ad-platform conversions to the account that clicked the ad, not just the person who paid, using company_external_id.
## The problem
In B2C, the person who clicks the ad is usually the person who buys — attribution just follows one contact. B2B breaks that assumption. Accounts payable pays the invoice; a developer on the same team clicked the ad and signed up for a free trial weeks earlier using a personal Gmail address. Grouping contacts by email domain can't connect them, because the payer's and the developer's email domains don't match, or neither address is a business address at all.
What you actually want is CRM-style **account-level attribution**: group every contact that belongs to the same customer account together, so a conversion by one teammate can be credited to the ad another teammate clicked. `company_external_id` is how you tell Cometly which contacts belong to the same account.
## Prerequisites
* A Cometly [API key](/introduction/authentication).
* A Space.
* The Space's **Company identity mode** set to **"Group by your account ID"** (External ID), in **Space Setup → Additional Setup → Company Tracking**. The other mode, **"Group by email domain"** (Auto), is the default and ignores `company_external_id` entirely.
Pick one mode and stick to it. Switching does not move existing contacts: companies that were grouped by email domain keep their current members and simply stop growing, and companies already grouped by external ID stay exactly as they are. Nothing is merged or re-grouped retroactively.
While a Space is in the default **Auto** mode, `company_external_id` is still accepted on events and stored — it's just ignored for grouping. If your events already carry the field while the Space is in Auto mode, the Company Tracking settings page shows a warning banner telling you so, so you don't spend time debugging a company grouping that was never turned on.
## The identifiers, contrasted
Three fields on [Create Event](/api-reference/endpoint/create-event) sound similar but do unrelated jobs. Mixing them up either merges contacts that shouldn't be merged, or fails to group the account at all.
| Field | Does | Never |
| - | - | - |
| `tracking_id` | Merges events into **one contact**. Use a stable per-user value. | Never groups companies. Never send a shared account ID here — every teammate would merge into a single contact. |
| `company_external_id` | Groups contacts into a **company** by your own account ID. | Never merges contacts, and never takes part in contact matching. |
| `idempotency_key` | An exact-dedup key, unique **per event**. Required for high-frequency pipelines — without a distinct key per event, repeated events can collapse into each other under Cometly's same-contact dedup window. | — |
`company_name` rides alongside `company_external_id` as a fourth field: it's a display-only name for the company, last write wins, and it's never used for matching — two companies with the same name stay separate. It's ignored unless `company_external_id` is also sent, since the name alone doesn't identify a company.
## Send it on every event
Send `company_external_id` on every event a member of the account triggers — sign-up, trial activation, purchase, whatever your funnel tracks — alongside a per-user `tracking_id`, a unique `idempotency_key`, and optionally `company_name`. Normalization rules for `company_external_id` and `company_name`: trimmed, case **preserved** (never lowercased), stored byte-exact, 1–190 characters (`company_external_id`) or 1–255 characters (`company_name`) after trimming, and integers are accepted. A blank string is treated as absent (`null`); floats, arrays, and booleans are rejected with a `422`.
A developer signs up with a personal address:
```bash cURL theme={null}
curl -X POST https://app.cometly.com/public-api/v1/events/track \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"event_name": "sign_up",
"email": "dev@gmail.com",
"tracking_id": "user_48213",
"idempotency_key": "evt_signup_48213_20260909",
"company_external_id": "acct_8f3k2",
"company_name": "Example Co",
"event_time": "2026-09-09T14:02:00Z"
}'
```
```javascript JavaScript theme={null}
const response = await fetch('https://app.cometly.com/public-api/v1/events/track', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
event_name: 'sign_up',
email: 'dev@gmail.com',
tracking_id: 'user_48213',
idempotency_key: 'evt_signup_48213_20260909',
company_external_id: 'acct_8f3k2',
company_name: 'Example Co',
event_time: '2026-09-09T14:02:00Z'
})
});
const data = await response.json();
```
```php PHP theme={null}
'sign_up',
'email' => 'dev@gmail.com',
'tracking_id' => 'user_48213',
'idempotency_key' => 'evt_signup_48213_20260909',
'company_external_id' => 'acct_8f3k2',
'company_name' => 'Example Co',
'event_time' => '2026-09-09T14:02:00Z',
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
url = 'https://app.cometly.com/public-api/v1/events/track'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'event_name': 'sign_up',
'email': 'dev@gmail.com',
'tracking_id': 'user_48213',
'idempotency_key': 'evt_signup_48213_20260909',
'company_external_id': 'acct_8f3k2',
'company_name': 'Example Co',
'event_time': '2026-09-09T14:02:00Z',
}
response = requests.post(url, headers=headers, json=data)
```
Weeks later, accounts payable pays the invoice from a different, business email address — same `company_external_id`, different contact:
```bash cURL theme={null}
curl -X POST https://app.cometly.com/public-api/v1/events/track \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"event_name": "purchase",
"email": "ap@example.com",
"tracking_id": "user_91004",
"idempotency_key": "evt_purchase_ord-77213",
"amount": 4999.00,
"order_id": "ORD-77213",
"company_external_id": "acct_8f3k2",
"company_name": "Example Co",
"event_time": "2026-09-25T09:15:00Z"
}'
```
```javascript JavaScript theme={null}
const response = await fetch('https://app.cometly.com/public-api/v1/events/track', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
event_name: 'purchase',
email: 'ap@example.com',
tracking_id: 'user_91004',
idempotency_key: 'evt_purchase_ord-77213',
amount: 4999.00,
order_id: 'ORD-77213',
company_external_id: 'acct_8f3k2',
company_name: 'Example Co',
event_time: '2026-09-25T09:15:00Z'
})
});
const data = await response.json();
```
```php PHP theme={null}
'purchase',
'email' => 'ap@example.com',
'tracking_id' => 'user_91004',
'idempotency_key' => 'evt_purchase_ord-77213',
'amount' => 4999.00,
'order_id' => 'ORD-77213',
'company_external_id' => 'acct_8f3k2',
'company_name' => 'Example Co',
'event_time' => '2026-09-25T09:15:00Z',
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
?>
```
```python Python theme={null}
import requests
url = 'https://app.cometly.com/public-api/v1/events/track'
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
data = {
'event_name': 'purchase',
'email': 'ap@example.com',
'tracking_id': 'user_91004',
'idempotency_key': 'evt_purchase_ord-77213',
'amount': 4999.00,
'order_id': 'ORD-77213',
'company_external_id': 'acct_8f3k2',
'company_name': 'Example Co',
'event_time': '2026-09-25T09:15:00Z',
}
response = requests.post(url, headers=headers, json=data)
```
Both events carry the same `company_external_id`, so once External ID mode is on, both contacts land in the same company — even though they never share a `tracking_id`, an email domain, or a name.
## Tag logged-in visitors from your website
The API and webhooks cover what your backend knows. The browser knows something too: the developer who clicked the ad and is now browsing your app while logged in. If your website can tell who the visitor is, declare their account once on the page and the Cometly tracking script includes it in **every event it sends** — page views, form events, and anything you fire through `comet()` — with no per-event code.
```html theme={null}
```
How it behaves:
* **Render it on every page where the visitor is logged in**, and leave it out when they are not. The script does not remember it between pages, so a shared computer never carries the previous user's account onto the next one.
* **Order does not matter.** Set the object before or after the script tag. The script reads it at the moment each event is sent, so a single-page app that finishes authenticating after the page loaded can assign `window.cometlySettings` at that point and the next event carries it.
* **Only these two keys are read.** Anything else in the object is ignored. The same normalization applies as on the API: trimmed, case preserved, integers accepted, blank treated as absent.
* **Direct values win.** If you pass `company_external_id` or `company_name` explicitly in `comet(event, data)`, that value is used for that event instead of the settings object.
* **External ID mode only.** Like every other way of sending the field, the object groups contacts only when the Space's company identity mode is **"Group by your account ID"**. In Auto mode it is accepted and ignored.
The first page view of a session usually fires before your app has rendered the object (the visitor is not logged in yet, or the SPA has not finished auth). That is fine: as soon as any later event from the same visitor carries the account, Cometly re-stamps that contact's earlier events, including the anonymous ad-click page view, onto the company.
Anything set in the browser can be changed by the visitor, from the developer console if nowhere else, exactly like an email typed into a form. Treat browser-supplied account IDs with the same trust as any other pixel data. For events that must be right, such as purchases, send the account ID from your server through the API or a webhook.
## Send it via webhooks or Stripe metadata
If you're sending events through a [webhook](/concepts/sending-data-to-cometly) instead of the API directly, `Company External ID` and `Company Name` are available as mapping destinations in the webhook mapping step, alongside your other field mappings.
For Stripe specifically, your account ID typically travels in the object's metadata rather than as a top-level field. Map the source field `data.object.metadata.` (for example `data.object.metadata.account_id`) to the **Company External ID** destination, and optionally another metadata key to **Company Name**.
The source-field dropdown in the mapping step is only populated from the sample payloads Cometly received while you were setting up the webhook (**Check for Data**). If your metadata key wasn't present on any of those sample events, it won't show up as an option. Send a test event carrying the metadata key first, then re-run **Check for Data**, before mapping it.
## Timing: send sign-up events in real time
Ad-platform conversions (CAPI) go out roughly 14 minutes after the conversion itself. For a teammate's ad click to be picked up when accounts payable's purchase is sent to Meta or Google, the developer's account membership (their `company_external_id`) needs to already be recorded by then. Send sign-up and membership events in real time as they happen, not batched into a nightly job — a sign-up event that arrives after the conversion it should inform has already been sent is too late to change how that conversion was attributed. Tagging logged-in visitors with `window.cometlySettings` (above) is the simplest way to get there: membership is recorded on the first page view after login, with no pipeline involved.
## Turn on ad-platform conversion identity = company
Company grouping alone doesn't change who a conversion is attributed to when it's sent to Meta, Google, and the other ad platforms — it only groups contacts for reporting. To also redirect the ad-platform conversion's identity to the right teammate, turn on the second Company Tracking setting: **Ad-platform conversion identity → "Send as the teammate who clicked the ad"** (`company` mode, on the same Company Tracking page).
With this on:
* A conversion is sent under the identity of the teammate in the same company whose ad click is the most recent before the conversion — their email, name, phone, and click IDs. It's always one person's identity, **never a blend** of two people.
* If no teammate in the company clicked an ad more recently, the conversion falls back to being sent as the person who actually converted.
* It never changes conversion facts: `amount`, `order_id`, and the conversion time are always the real ones.
* It requires company grouping — a contact with no company (in either identity mode) is always sent as themselves.
## Fix a grouping, or link a contact that never sends events
Some contacts never send an event carrying `company_external_id` at all — an inbound lead created via CRM sync, for instance. Two endpoints let you fix that without waiting for another event:
* [Update Company](/api-reference/endpoint/update-company) — set or change a company's `external_id` directly.
* [Attach Contact Company](/api-reference/endpoint/attach-contact-company) / [Detach Contact Company](/api-reference/endpoint/detach-contact-company) — move a contact onto a company, or off it entirely.
What happens after a manual attach or detach 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 (it isn't undone by a later event) and a manual detach is undone by the contact's next business-email event. In **External ID** mode, the next event carrying a `company_external_id` re-points the contact again either way.
## Read it back
* [Get Company](/api-reference/endpoint/get-company) and [List Companies](/api-reference/endpoint/list-companies) both return `external_id` alongside `domain` and `name`.
* The [Companies dataset](/data-warehouse/datasets/companies) in the data warehouse carries `external_id` as a column — join on an exact match against your own account IDs.
* If you're **enabling External ID mode on a Space that already has history**, run a one-time export over all history on the Companies dataset. Incremental exports are windowed on `created_at`, so a company that gains an `external_id` after its original creation window won't be re-exported by a recurring run.
## Reality note
Cometly groups companies by business-email domain (Auto mode) or by your own `company_external_id` (External ID mode) — it does not read association data from HubSpot, Salesforce, or any other CRM's company/account records. If you need Cometly's company grouping to match your CRM's account structure, send that CRM's account identifier as `company_external_id` on every event, rather than expecting CRM associations to be picked up automatically.
# Cometly API Documentation
Source: https://docs.cometly.com/index
Welcome to the Cometly API documentation. Learn how to integrate and use our API to track conversions and manage your marketing data.
## Overview
The Cometly API provides a RESTful interface to interact with your Cometly account programmatically. Use our API to send conversion events, manage tracking data, and integrate Cometly with your applications.
### Base URL
```
https://app.cometly.com/public-api/v1
```
## Support
Need help with the API? Check out our resources:
Visit our help center for guides, tutorials, and FAQs.
# Authentication
Source: https://docs.cometly.com/introduction/authentication
Learn how to authenticate your requests to the Cometly API
## Overview
The Cometly API uses API keys to authenticate requests. You can view and manage your API keys in the Cometly Dashboard.
## API Key Authentication
All API requests must include your API key in the request headers. Your API keys carry many privileges, so be sure to keep them secure. Do not share your secret API keys in publicly accessible areas such as GitHub, client-side code, etc.
### Authentication Header
Include your API key in the `Authorization` header using the Bearer authentication scheme:
```bash theme={null}
Authorization: Bearer YOUR_API_KEY
```
### Required Headers
All API requests must include the following headers:
* **Authorization**: Your API key using Bearer authentication scheme
* **Accept**: `application/json` - Indicates that you expect JSON responses
* **Content-Type**: `application/json` - Specifies that you're sending JSON data
```bash theme={null}
Authorization: Bearer YOUR_API_KEY
Accept: application/json
Content-Type: application/json
```
## Example Request
Here's an example of an authenticated request:
```bash cURL theme={null}
curl https://app.cometly.com/public-api/v1/events \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json"
```
```javascript JavaScript theme={null}
fetch('https://app.cometly.com/public-api/v1/events', {
method: 'GET',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
```
```php PHP theme={null}
```
```python Python theme={null}
import requests
headers = {
'Authorization': 'Bearer YOUR_API_KEY',
'Accept': 'application/json',
'Content-Type': 'application/json'
}
response = requests.get('https://app.cometly.com/public-api/v1/events', headers=headers)
```
## Getting Your API Key
Navigate to [app.cometly.com](https://app.cometly.com) and log in.
Navigate to the Integrations page at [app.cometly.com/integrations](https://app.cometly.com/integrations).
Use the search bar to find "cometly api" and click on the Cometly API integration page.
Click the button to generate a new API key. Make sure to copy and save it immediately, as you won't be able to view it again.
## Error Responses
If authentication fails, you'll receive one of the following error responses:
| Status Code | Error | Description |
| - | - | - |
| 401 | Unauthenticated | Missing or invalid API key |
| 403 | Forbidden | API key doesn't have permission for this resource, or your Cometly subscription is inactive |
# Rate Limiting
Source: https://docs.cometly.com/introduction/rate-limiting
Understand API rate limits and how to handle them in your application
## Overview
The Cometly API implements rate limiting to ensure fair usage and maintain service quality for all users. Rate limits are applied per Space and are enforced per minute. Each endpoint has specific rate limits documented on its respective page.
## Rate Limit Headers
Every API response includes headers that provide information about your current rate limit status:
| Header | Description |
| - | - |
| `X-RateLimit-Limit` | The maximum number of requests allowed in the current time window |
| `X-RateLimit-Remaining` | The number of requests remaining in the current time window |
| `Retry-After` | (Only on 429 responses) Number of seconds to wait before retrying |
## Rate Limit Response
When you exceed a rate limit, the API will return a `429 Too Many Requests` status code.
The response will include a `Retry-After` header indicating how many seconds you should wait before making another request.
## Need Higher Limits?
If your use case requires higher rate limits, please contact our support team at [support@cometly.com](mailto:support@cometly.com) to discuss custom rate limit options for your account.
# Status Codes
Source: https://docs.cometly.com/introduction/status-codes
HTTP status codes and error handling in the Cometly API
## Overview
The Cometly API uses conventional HTTP response codes to indicate the success or failure of an API request. Codes in the `2xx` range indicate success, and codes in the `4xx` range indicate an error with the request.
## HTTP Status Code Summary
| Status Code | Description |
| - | - |
| 200 | OK - The request was successful |
| 201 | Created - The resource was successfully created |
| 202 | Accepted - The request has been queued for processing successfully |
| 401 | Unauthenticated - Authentication failed or wasn't provided |
| 403 | Forbidden - The request is understood but refused |
| 404 | Not Found - The requested resource does not exist |
| 422 | Unprocessable Entity - Request validation failed or missing required parameters |
## Error Response Structure
All error responses follow a consistent structure:
```json theme={null}
{
"message": "Human-readable error message"
}
```
| Field | Type | Description |
| - | - | - |
| `message` | string | Human-readable error description |
# Authentication
Source: https://docs.cometly.com/mcp/authentication
OAuth 2.1 with PKCE for browser-based clients (CIMD or Dynamic Client Registration), or a personal access token for server-side agents. MCP-aware clients like Claude Desktop and Cursor handle OAuth automatically.
## TL;DR
You don't need to manage anything by hand. MCP-aware clients walk the full OAuth flow themselves the first time they hit the Cometly MCP endpoint:
1. The client sees a `401 Unauthorized` with a `WWW-Authenticate` header pointing at our OAuth discovery URL.
2. The client identifies itself — either via a hosted metadata document (CIMD) or by calling the registration endpoint (DCR).
3. The client opens your browser to the Cometly authorization page.
4. You log in, pick which Space to grant access to, and click **Approve**.
5. The client exchanges the resulting code for an access token bound to that Space.
After that the client just sends `Authorization: Bearer ` on every tool call.
If you're **building a custom MCP client**, you have two registration paths: [CIMD](#client-id-metadata-document-cimd) (preferred — no registration call required) and [Dynamic Client Registration](#dynamic-client-registration) (fallback, e.g. for custom URI schemes). If you want the gritty detail, keep reading.
If you're connecting a **server-side agent** (headless script, CI job, backend service — no browser), skip OAuth entirely and use a [personal access token](#server-side-agents-pat) instead.
## Endpoints
Cometly is its own authorization server. There's no third-party identity provider in the middle.
| Endpoint | URL |
| - | - |
| Protected resource metadata (RFC 9728) | `https://app.cometly.com/.well-known/oauth-protected-resource/{path}` |
| Authorization server metadata (RFC 8414) | `https://app.cometly.com/.well-known/oauth-authorization-server/{path}` |
| Dynamic client registration | `POST https://app.cometly.com/mcp/oauth/register` |
| Authorization (browser) | `GET https://app.cometly.com/mcp/oauth/authorize` |
| Token exchange | `POST https://app.cometly.com/mcp/oauth/token` |
| MCP server (resource) | `https://app.cometly.com/mcp/{space_id}/public-api` |
We expose **both** the path-prefix (RFC 9728) and path-suffix (RFC 8414) `.well-known` URL variants because different clients implement different revisions of the spec. Claude Desktop uses the prefix form; Cursor uses the suffix form. Both resolve to the same metadata.
## Discovery metadata
Hitting the auth-server metadata endpoint returns:
```json theme={null}
{
"issuer": "https://app.cometly.com",
"authorization_endpoint": "https://app.cometly.com/mcp/oauth/authorize",
"token_endpoint": "https://app.cometly.com/mcp/oauth/token",
"registration_endpoint": "https://app.cometly.com/mcp/oauth/register",
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["mcp:use"],
"grant_types_supported": ["authorization_code"],
"client_id_metadata_document_supported": true
}
```
PKCE with `S256` is required. There's no implicit grant, no password grant, no client-credentials grant — just authorization code with PKCE.
## Client ID Metadata Document (CIMD)
CIMD is the preferred registration path for new custom MCP clients. Instead of calling `/mcp/oauth/register` to get a client ID, you host a JSON document at an `https://` URL and use that URL itself as your `client_id` — no registration request needed.
Support is advertised via `client_id_metadata_document_supported: true` in the [discovery metadata](#discovery-metadata). If that field is absent or `false`, fall back to [Dynamic Client Registration](#dynamic-client-registration).
### Metadata document
Host a JSON file at any stable `https://` URL with a non-root path (for example, `https://app.example.com/oauth/client-metadata.json`). The document must include:
| Field | Required | Description |
| - | - | - |
| `client_id` | Yes | Must exactly equal the URL the document is hosted at. |
| `client_name` | Yes | Shown verbatim on the consent screen. |
| `redirect_uris` | Yes | Array of allowed redirect URIs — see rules below. |
| `token_endpoint_auth_method` | Yes | Must be `"none"`. Public clients only; `private_key_jwt` is not yet supported. |
All other fields (`client_uri`, `grant_types`, `response_types`) are accepted and recorded but not enforced.
```json theme={null}
{
"client_id": "https://app.example.com/oauth/client-metadata.json",
"client_name": "Example MCP Client",
"client_uri": "https://app.example.com",
"redirect_uris": ["https://app.example.com/callback", "http://127.0.0.1:3000/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
```
On the consent screen, Cometly shows the verified client domain alongside `client_name`. Clients whose only redirect URIs are loopback addresses receive an advisory notice on the consent screen.
### Accepted `redirect_uris` for CIMD clients
Each URI in the document must satisfy one of two rules:
| Category | Examples | Rule |
| - | - | - |
| Loopback | `http://127.0.0.1:33333/callback`, `http://localhost/cb` | Any port, any path. RFC 8252 loopback normalization applies. |
| Same-origin https | `https://app.example.com/callback` | The redirect URI origin must exactly match the `client_id` URL origin (same scheme, host, and port). Cross-origin https URIs are rejected. |
**Custom URI schemes** (e.g. `cursor://`, `vscode://`) are **not** accepted for CIMD clients. If your client uses a custom scheme, use [Dynamic Client Registration](#dynamic-client-registration) instead — the custom-scheme allowlist applies only to DCR.
The `redirect_uri` you send on `/authorize` and `/token` must exactly match one of the URIs in the document (loopback port normalization per RFC 8252 applies).
### Document fetching and caching
On every `/authorize` request with a URL-shaped `client_id`, Cometly fetches and validates the metadata document. The fetch is:
* **SSRF-guarded** — private, loopback, link-local, and cloud-metadata IPs are refused.
* **TLS-verified** — the certificate must be valid; redirects are not followed by default.
* **Rate-limited and size-capped** — oversized or excessively frequent responses are rejected.
Cometly caches the validated document per `client_id` URL, honoring `Cache-Control: max-age` up to a 24-hour ceiling (default window 15 minutes). Deploy an updated document and wait up to 24 hours for all caches to clear, or set a shorter `max-age` on your server's response.
## Dynamic client registration
Dynamic Client Registration (RFC 7591) is the fallback path when CIMD isn't suitable — for example when your client uses a custom URI scheme (`cursor://`, `vscode://`) that CIMD doesn't accept, or when you can't host a metadata document at a stable https URL.
`POST /mcp/oauth/register` with the client's metadata. Minimal example:
```json theme={null}
{
"client_name": "My MCP Client",
"redirect_uris": ["http://127.0.0.1:33333/callback"],
"token_endpoint_auth_method": "none"
}
```
Returns a `client_id` you'll use for the authorization request. There is no `client_secret` — Cometly's MCP server treats every client as a public client and relies on PKCE for security.
### Accepted `redirect_uris`
The redirect URI is validated at registration. Each URI must fall into one of three accepted categories; redirect URIs that don't match are rejected. This allowlist applies to DCR clients only — [CIMD clients](#accepted-redirect_uris-for-cimd-clients) use a same-host rule instead.
| Category | Examples | Use case |
| - | - | - |
| Loopback | `http://127.0.0.1:33333/callback`, `http://localhost/cb` | Native desktop clients |
| `https://` to an allowed host | `https://claude.ai/api/mcp/auth_callback` | Hosted web clients — see [Default allowed https hosts](#default-allowed-https-hosts) below |
| Well-known custom scheme | `cursor://...`, `vscode://...`, `claude-desktop://...`, `com.example.app:/oauth2redirect` | Desktop and native apps |
### Default allowed https hosts
These hosts are accepted for DCR clients out of the box. If you need a host that isn't listed, contact support: [support@cometly.com](mailto:support@cometly.com). (CIMD clients don't need to be on this list — they use the same-host rule described above.)
| Host(s) | Client |
| - | - |
| `claude.ai`, `claude.com` | Anthropic Claude (web, desktop, mobile) |
| `chatgpt.com`, `chat.openai.com` | OpenAI ChatGPT |
| `cursor.com`, `www.cursor.com` | Cursor web agents |
| `vscode.dev`, `insiders.vscode.dev` | VS Code for the Web / github.dev (desktop VS Code uses loopback + `vscode://`) |
| `grok.com` | xAI Grok |
| `gemini.google.com` | Google Gemini |
| `perplexity.ai`, `www.perplexity.ai` | Perplexity |
### Accepted `client_name`
`client_name` is shown verbatim on the consent screen, so it's validated to prevent impersonation and UI hijacking:
* Maximum 120 characters.
* No control characters (`\x00–\x1f`, `\x7f`).
* Names that contain `cometly` as a standalone word (case-insensitive, word/whitespace/hyphen/underscore boundaries) are rejected to keep malicious clients from passing themselves off as Cometly itself.
### Rate limits
`POST /mcp/oauth/register` is throttled to **10 registrations per hour per IP**. Legitimate clients register once per install, so this leaves plenty of headroom for retries while making it impractical to flood the registration table. Exceeded requests return `429 temporarily_unavailable` with an RFC 7591 error body.
## Authorization flow
This flow is the same whether your `client_id` is an opaque string from DCR or an https URL from CIMD.
1. Generate a PKCE `code_verifier` and the matching `code_challenge` (`SHA256(verifier)`, base64url, no padding).
2. Redirect the user (in their browser) to:
```
https://app.cometly.com/mcp/oauth/authorize
?response_type=code
&client_id={client_id}
&redirect_uri={redirect_uri}
&code_challenge={code_challenge}
&code_challenge_method=S256
&state={random}
&scope=mcp:use
```
3. The user logs in (if not already), then picks the Space and clicks **Approve**.
4. Cometly redirects back to your `redirect_uri` with `?code={code}&state={state}`.
5. Exchange the code:
```bash theme={null}
curl -X POST https://app.cometly.com/mcp/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code={code}" \
-d "redirect_uri={redirect_uri}" \
-d "client_id={client_id}" \
-d "code_verifier={code_verifier}"
```
Response:
```json theme={null}
{
"access_token": "1|abc...",
"token_type": "Bearer",
"expires_in": null,
"scope": "mcp_space:42"
}
```
## Server-side agents (PAT)
The [Authorization flow](#authorization-flow) above is the right default for MCP-aware clients with a browser. If you're building a **server-side agent** (a headless script, a CI job, a backend service), there's no human at a browser to click **Approve**. For those cases, generate a personal access token (PAT) in the Cometly dashboard and send it as a bearer header.
A personal access token behaves exactly like an OAuth-issued token: it's tied to your user account, works for one Space only, and your access to that Space is re-checked on every request. The only difference is how you obtain one.
### Generate a token
Before you start, make sure you're signed in to Cometly and can see the target Space's **Settings → Integrations → MCP** page. If you can't, ask a Space admin to grant you API access.
1. Open **Settings → Integrations → MCP** in your Cometly dashboard.
2. Under **MCP Access Tokens**, enter a name for the token (e.g. `CI Agent`) and click **Generate Token**.
3. Copy the token right away. For security reasons, it isn't shown again. A ready-to-paste config snippet is displayed alongside it.
### Configure your agent
The token works with any MCP client that supports `type: http` servers with bearer auth. Minimal `.mcp.json`:
```json theme={null}
{
"mcpServers": {
"cometly-agent": {
"type": "http",
"url": "https://app.cometly.com/mcp/42/public-api",
"headers": {
"Authorization": "Bearer 1|abc..."
}
}
}
}
```
If you're calling the MCP endpoint directly (no MCP client library), see [Calling the MCP server](#calling-the-mcp-server). The `Authorization: Bearer` header is the only difference from an OAuth-issued token.
### Limits
* You can generate tokens on this page until you hold **25 MCP tokens for the Space**. Tokens issued to OAuth clients (Cursor, Claude Desktop, etc.) count toward that number, so if you hit the limit, revoke tokens you no longer use from the same page.
* The token works only for the Space you generated it in. There is no "all Spaces" token.
* Token names are limited to 120 characters and can't contain control characters. The name is only a label for the list on the settings page; it has no effect on what the token can do.
* Tokens don't expire. Revoke them when an agent is decommissioned (see [Revoking access](#revoking-access)).
## Calling the MCP server
Once you have an access token, every JSON-RPC call to the MCP endpoint carries it:
```bash theme={null}
curl -X POST https://app.cometly.com/mcp/42/public-api \
-H "Authorization: Bearer 1|abc..." \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
The Space ID in the URL **must** match the Space the token was approved for — Cometly checks the token's `mcp_space:{id}` ability against the route param on every request and returns `403` if they disagree.
## Scopes
There's a single MCP scope today:
| Scope | What it grants |
| - | - |
| `mcp_space:{id}` | Call any tool (including the three `update_*` write tools) against Space `{id}`. The Space ID is bound into the scope string — a token approved for Space 42 cannot read Space 43. |
When you click **Approve** in the consent UI, the token's ability is set to exactly one `mcp_space:{id}` value. There's no broader "all spaces" scope, by design.
## Revoking access
You can revoke any MCP token for a Space — whether issued via OAuth (Cursor, Claude Desktop, etc.) or generated as a PAT — from a single list:
1. Open **Settings → Integrations → MCP** in your Cometly dashboard.
2. Under **Server-side (headless) agents**, find the token you want to revoke. OAuth-issued tokens appear here too, named after the client that requested them (e.g. `MCP: Cursor (Acme Inc.)`).
3. Click **Revoke**.
The next tool call from that token gets `401 Unauthorized`. OAuth clients will walk the auth flow again on next launch; PAT clients will need a fresh token generated for them.
Removing a user from a Space also immediately revokes every MCP token they hold for that Space — there's no separate cleanup step.
## Common errors
| Status | Meaning | Recovery |
| - | - | - |
| `401` | No / invalid / expired token. | Walk the OAuth flow again. The `WWW-Authenticate` header includes the discovery URL. |
| `403` | Token's `mcp_space:{id}` doesn't match the URL's Space ID, or the user no longer has API permission in that Space. | Re-authorize with the correct Space. |
| `403` (subscription) | The Space's team doesn't have an active Public API subscription. | Reach out to your Cometly admin or contact sales. |
| `429` | Rate limit exceeded. | Back off. See [Rate Limits & Errors](/mcp/rate-limits-and-errors). |
# ChatGPT
Source: https://docs.cometly.com/mcp/connecting/chatgpt
Connect the Cometly MCP server to ChatGPT's Desktop app (and any other MCP-aware OpenAI client) so you can ask GPT-5 about your marketing data.
## Prerequisites
* A Cometly workspace and a Space you have **Use Public API** permission in.
* ChatGPT Desktop app with **Developer Mode → MCP Connectors** enabled.
* Your numeric **Space ID** — copy it from the **ID** row at the bottom of the dashboard sidebar, or from [**Space Settings**](https://app.cometly.com/space/settings) → **Space Details**. See [Finding your Space ID](/mcp/overview#finding-your-space-id) for step-by-step instructions.
MCP support in ChatGPT is rolling out gradually and may require an enabled Developer Mode preview on your account. The connect surface looks slightly different depending on whether you're on Plus, Pro, or Business — the steps below are for the standard Developer Mode connector flow.
## Add the connector
In ChatGPT Desktop, open **Settings → Connectors → Add MCP server** (under Developer Mode).
Fill in:
| Field | Value |
| - | - |
| Name | `Cometly` |
| Transport | `Streamable HTTP` |
| URL | The endpoint below, with your Space ID filled in |
Copy the endpoint URL and replace `{your-space-id}` with your numeric Space ID:
```text theme={null}
https://app.cometly.com/mcp/{your-space-id}/public-api
```
Click **Save**.
## Authorize
ChatGPT will open your browser to the Cometly authorization page on first use. Log in, confirm the Space, and click **Approve**. The token is saved inside ChatGPT's connector store — you can revoke it at any time from the same Connectors screen.
## Verify
Start a new chat with `gpt-5` or higher selected (smaller models occasionally hallucinate MCP tool calls). Ask:
> Using the Cometly connector, show my purchase volume by source for the last 7 days.
You should see a `query_ad_metrics` tool call followed by a real response.
## Troubleshooting
You're not on a build that supports MCP yet, or Developer Mode is off. Update ChatGPT Desktop and toggle on **Settings → Beta features → Developer Mode**. If that toggle isn't present, your account doesn't have access yet.
ChatGPT's connector layer occasionally races on the OAuth callback. Restart the app, remove the connector, and add it again — the second attempt is usually clean.
The token is missing or stale. Open the connector settings, click **Reconnect**, and walk the OAuth flow again.
## Privacy note
ChatGPT logs prompts and tool calls per OpenAI's data policy. The MCP server itself never sees your prompt — only the tool name and validated arguments your client sends. Errors are logged with a `correlation_id` only; no PII from arguments is digested into our audit log.
# Claude Code
Source: https://docs.cometly.com/mcp/connecting/claude-code
Use the Cometly MCP server from Claude Code (Anthropic's CLI) so your terminal agent can pull live marketing data.
## Prerequisites
* A Cometly workspace and a Space you have **Use Public API** permission in.
* [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed.
* Your numeric **Space ID** — copy it from the **ID** row at the bottom of the dashboard sidebar, or from [**Space Settings**](https://app.cometly.com/space/settings) → **Space Details**. See [Finding your Space ID](/mcp/overview#finding-your-space-id) for step-by-step instructions.
## Add the server
From your terminal:
```bash theme={null}
claude mcp add --transport http cometly https://app.cometly.com/mcp/42/public-api
```
Replace `42` with your Space ID. Claude Code stores the config in `~/.claude/config.json`. To inspect what was saved:
```bash theme={null}
claude mcp list
```
## Authorize
The first time Claude Code uses the server, it'll print an OAuth URL to your terminal. Open it in your browser, log in, pick the Space, and click **Approve**. Claude Code completes the handshake and caches the token at `~/.claude/mcp/cometly/credentials.json`.
## Verify
Start a new session and ask:
```bash theme={null}
claude
```
```
> Using the cometly MCP server, show me my ROAS by source for the last 30 days.
```
You should see a `query_ad_metrics` tool call followed by a real answer. If you'd rather invoke the server programmatically (e.g. from a CI job), you can pipe a prompt straight in:
```bash theme={null}
claude -p "Using the cometly MCP server, list contacts created this week where Customer Plan is Premium" --output-format=stream-json
```
## Troubleshooting
Confirm the URL is correct and that you replaced `{space_id}` with your numeric Space ID. To re-walk OAuth, remove the cached credentials:
```bash theme={null}
rm -rf ~/.claude/mcp/cometly
```
Then ask Claude to use the server again — it'll re-authorize.
Make sure you're on a recent Claude Code version that supports HTTP transport MCP servers. `claude --version` should be at least the version that ships with `claude mcp add --transport http`.
The token was approved for a different Space than the one in the URL, or you lost API permission. Remove the credentials and re-authorize, picking the correct Space.
## Common patterns
* **One-shot analysis in CI:**
```bash theme={null}
claude -p "Use the cometly server to pull yesterday's purchase volume by source and write the summary to ./reports/$(date -I).md"
```
* **Interactive with explicit Space switching:** add multiple servers under different names if you regularly work across spaces.
```bash theme={null}
claude mcp add --transport http cometly-prod https://app.cometly.com/mcp/42/public-api
claude mcp add --transport http cometly-staging https://app.cometly.com/mcp/99/public-api
```
Then prompt with the explicit name: *"Using `cometly-prod`, …"*
* **Scoped tool restriction:** Claude Code respects per-server `--allow-tools` flags if you want to limit a session to just read-only metric queries.
```bash theme={null}
claude --allow-tools "mcp:cometly:query_ad_metrics,mcp:cometly:list_events"
```
# Claude Desktop
Source: https://docs.cometly.com/mcp/connecting/claude-desktop
Install the Cometly MCP server in Claude Desktop and ask Claude about your marketing data in plain English.
**Claude web also works out of the box.** If you prefer to use Cometly in the [claude.ai](https://claude.ai) web interface or mobile app, add the connector there instead — `claude.ai` and `claude.com` work out of the box. The steps are the same: paste the MCP endpoint URL and approve access on first use.
## Prerequisites
* A Cometly workspace and a Space you have **Use Public API** permission in.
* Claude Desktop (latest — MCP support is in all current builds).
* Your numeric **Space ID** — copy it from the **ID** row at the bottom of the dashboard sidebar, or from [**Space Settings**](https://app.cometly.com/space/settings) → **Space Details**. See [Finding your Space ID](/mcp/overview#finding-your-space-id) for step-by-step instructions.
## Add the server
In Claude Desktop, open **Settings → Connectors** (or **Settings → MCP** depending on your build).
Click **Add custom connector** and fill in:
| Field | Value |
| - | - |
| Name | `Cometly` |
| URL | The endpoint below, with your Space ID filled in |
Copy the endpoint URL and replace `{your-space-id}` with your numeric Space ID:
```text theme={null}
https://app.cometly.com/mcp/{your-space-id}/public-api
```
Click **Save**.
Or, if you'd rather edit the config file directly, open the Claude Desktop config:
* **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
* **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
…and add:
```json theme={null}
{
"mcpServers": {
"cometly": {
"type": "http",
"url": "https://app.cometly.com/mcp/42/public-api"
}
}
}
```
Replace `42` with your Space ID, save, and restart Claude Desktop.
## Authorize
The first time Claude tries to call a Cometly tool, it'll open your browser to the Cometly authorization page. Log in, confirm the Space, and click **Approve**.
You'll see a green dot next to `Cometly` in the connectors list once the handshake succeeds.
## Verify
Open a new chat and ask:
> Using the Cometly connector, list my conversion events from the last 7 days.
You should see Claude call the `list_events` tool and respond with real data. The first response is sometimes a little slower (cold connection); subsequent calls are fast.
## Troubleshooting
Make sure you fully quit and reopened Claude Desktop after editing `claude_desktop_config.json` — it only reads the config at startup.
Older Claude Desktop builds occasionally swallow the OAuth URL. Re-add the connector via the **Connectors** UI instead of the config file; the UI flow is more reliable.
Your access token is missing or wrong. Remove the connector, restart Claude Desktop, and add it back — you'll re-walk the OAuth flow.
## What you can ask
* *"Show me purchases attributed to Facebook over the last 14 days with linear attribution."*
* *"Compare CPA between Google and Meta this month vs last month."*
* *"Find every contact created this week where Customer Plan is Premium."*
* *"Pull a full journey for contact 8231 and summarize it."*
Claude will pick the right tool ([`query_ad_metrics`](/mcp/tools/query-ad-metrics), [`list_events`](/mcp/tools/list-events), [`list_contacts`](/mcp/tools/list-contacts), [`get_contact`](/mcp/tools/get-contact)…) automatically and refer to your custom fields by their configured labels in its response.
# Cursor
Source: https://docs.cometly.com/mcp/connecting/cursor
Add the Cometly MCP server to Cursor and ask questions about your marketing data right from the chat sidebar.
## Prerequisites
* A Cometly workspace and a Space you have **Use Public API** permission in.
* Cursor 0.42 or newer (any version with MCP support).
* Your numeric **Space ID** — copy it from the **ID** row at the bottom of the dashboard sidebar, or from [**Space Settings**](https://app.cometly.com/space/settings) → **Space Details**. See [Finding your Space ID](/mcp/overview#finding-your-space-id) for step-by-step instructions.
## Add the server
Open Cursor's settings (`Cmd + ,` on macOS, `Ctrl + ,` on Windows / Linux), search for **MCP**, and click **+ Add new MCP server**. Set:
| Field | Value |
| - | - |
| Name | `cometly` |
| Type | `HTTP` |
| URL | `https://app.cometly.com/mcp/{your-space-id}/public-api` |
Click **Save**.
Or, if you'd rather edit the config file directly, open `~/.cursor/mcp.json` and add:
```json theme={null}
{
"mcpServers": {
"cometly": {
"url": "https://app.cometly.com/mcp/42/public-api"
}
}
}
```
Replace `42` with your Space ID.
## Authorize
The first time Cursor tries to talk to the server, it'll open your browser to the Cometly authorization page. Log in, confirm you're authorizing the right Space, and click **Approve**. Cursor takes over from there and saves the token automatically.
## Verify
Back in Cursor, open the chat sidebar and type:
```
Using the cometly MCP server, list my conversion events from the last 7 days.
```
You should see a tool indicator pop up (`list_events`), then a response with real data from your Space. If you see `401 Unauthorized`, your token expired — just send the message again and Cursor will re-walk the OAuth flow.
## Troubleshooting
Confirm the URL is `https://app.cometly.com/mcp/{space_id}/public-api` (no trailing slash) and that you replaced `{space_id}` with a real numeric ID. Cursor caches a failed handshake for a minute or so — wait and try again.
The token was approved for a different Space. Open `~/.cursor/mcp.json`, delete the cometly entry, restart Cursor, and re-add the server. Cursor will walk the OAuth flow again and you can pick the right Space.
Make sure Cursor is using the latest Claude or GPT model. Some smaller models hallucinate MCP tool names — bigger frontier models read the input schema and follow it.
## What you can ask
* *"Show me my top 10 ad sets by ROAS for the last 30 days."*
* *"List purchases from contacts where Customer Plan is Premium this month."*
* *"What was my CPA on Meta last week vs the week before?"*
* *"Look up contact 12345 and tell me their full journey."*
Cursor will pick the right tool ([`query_ad_metrics`](/mcp/tools/query-ad-metrics), [`list_events`](/mcp/tools/list-events), [`get_contact`](/mcp/tools/get-contact)…) automatically. You don't have to name the tool.
# MCP Overview
Source: https://docs.cometly.com/mcp/overview
Connect Claude Desktop, Cursor, and any MCP-aware client to your Cometly workspace and let it query your marketing analytics in natural language.
## What MCP is
The [Model Context Protocol](https://modelcontextprotocol.io/) is an open standard that lets AI clients (Claude Desktop, Cursor, Claude Code, ChatGPT's Desktop app, your own custom agents) discover and call tools on a remote server. Cometly exposes its analytics data as an MCP server so you can ask your AI tools things like:
* *"What was my ROAS by source last month?"*
* *"List contacts created this week where Customer Plan is Premium."*
* *"Compare CPA across Meta and Google for the last 30 days vs the prior 30 days."*
…and the model will pick the right tool, call it against your workspace's live data, and answer in plain English with charts and tables.
## The endpoint
Every Cometly Space gets a single MCP endpoint:
```
https://app.cometly.com/mcp/{space_id}/public-api
```
Replace `{space_id}` with the numeric ID of the Space you want the client to talk to. See [Finding your Space ID](#finding-your-space-id) below.
## Finding your Space ID
Every MCP URL embeds a numeric Space ID. You'll need it when configuring Cursor, Claude Desktop, ChatGPT, or any other client.
**From the dashboard sidebar (fastest)** — In the Cometly app, look at the **bottom of the left navigation menu**. Under the **Support** heading (above Help Center), you'll see a compact **ID** row with your current Space's numeric ID and a **copy** icon. Click the icon to copy the ID, then paste it into your MCP endpoint URL.
**From Space Settings** — Open [**Space Settings**](https://app.cometly.com/space/settings). Scroll to the **Space Details** section at the bottom of the page and find **Space ID** — click the copy icon next to the number.
You can also reach that page from the dashboard: go to **Setup → Additional Setup → Manage Space Settings**.
The ID is just the number (for example, `6116`). Plug it into the endpoint above:
```
https://app.cometly.com/mcp/6116/public-api
```
## What you can do
The server exposes **50+ tools** across 14 toolsets — workspace orientation, search, analytics, events, contacts, companies, ads-manager hierarchy, reports, segments, integrations, health, diagnostics, webhooks, and AI helpers. Most tools wrap the same engines that power Cometly's report builder, Public API V1, and in-app dashboards; a handful (saved reports, webhook debugging, site inspection) are MCP-native.
Nearly every tool is **read-only**. Three write tools exist — `update_contact`, `update_company`, and [`update_event`](/mcp/tools/update-event) — and each one edits fields on a single record you address by ID. No tool can create or delete data in your workspace, and none can change attribution.
Call [`get_workspace_context`](/mcp/tools/get-workspace-context) once at session start (or subscribe to the `cometly://workspace/context` [Resource](/mcp/resources/overview)) so the model knows your timezone, currency, connected ad accounts, and custom event labels before it starts querying.
### Toolsets at a glance
| Toolset | Highlights |
| - | - |
| **Workspace** | [`get_workspace_context`](/mcp/tools/get-workspace-context), [`list_custom_events`](/mcp/tools/list-custom-events), [`resolve_date_range`](/mcp/tools/resolve-date-range) |
| **Search** | [`search_workspace`](/mcp/tools/search-workspace) — fuzzy lookup across campaigns, ads, contacts, reports, and more |
| **Analytics** | [`query_ad_metrics`](/mcp/tools/query-ad-metrics), [`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) |
| **Events** | [`list_events`](/mcp/tools/list-events), [`query_events_aggregated`](/mcp/tools/query-events-aggregated), [`list_event_types`](/mcp/tools/list-event-types), [`update_event`](/mcp/tools/update-event) (write) |
| **Contacts & Companies** | List/detail/journey tools plus [`query_contacts_aggregated`](/mcp/tools/query-contacts-aggregated) |
| **Ads Manager** | [`list_ad_accounts`](/mcp/tools/list-ad-accounts) through [`get_ad_entity`](/mcp/tools/get-ad-entity) |
| **Reports** | Dashboards, saved reports, [`run_saved_report`](/mcp/tools/run-saved-report), [`analyze_report`](/mcp/tools/analyze-report) (deducts AI credits) |
| **Segments** | [`list_segments`](/mcp/tools/list-segments), [`query_segment_membership`](/mcp/tools/query-segment-membership) |
| **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), [`inspect_site`](/mcp/tools/inspect-site), [`verify_pixel_events`](/mcp/tools/verify-pixel-events) |
| **Webhooks** | [`list_recent_webhooks`](/mcp/tools/list-recent-webhooks), [`get_webhook_event`](/mcp/tools/get-webhook-event) — payloads PII-redacted |
| **AI helpers** | [`search_cometly_docs`](/mcp/tools/search-cometly-docs), [`get_chart_spec_template`](/mcp/tools/get-chart-spec-template) |
See the full catalog with parameters and examples in the [Tools Reference](/mcp/tools/overview).
### Resources & Prompts
Beyond tools, the server exposes:
* **5 Resources** — subscribable reference data (`cometly://catalog/metrics`, `cometly://catalog/custom-events`, etc.). See [Resources](/mcp/resources/overview).
* **4 Prompts** — reusable slash-command templates like `weekly_review` and `analyze_performance`. See [Prompts](/mcp/prompts/overview).
### Bulk extraction
The MCP is for *analysis*, not bulk file export. As of **v2.1.0**, the four export tools (`request_event_export`, `request_contact_export`, `request_company_export`, `get_export_status`) were removed. For large datasets, use the [Public API V1 export endpoints](/api-reference/endpoint/create-event-export) or the in-app CSV export buttons instead of routing rows through an LLM turn.
## How it's secured
Connections use either **OAuth 2.1 with PKCE** (browser-based clients) or a **personal access token** (server-side agents). MCP-aware clients like Claude Desktop and Cursor use OAuth automatically. The first time they hit the endpoint:
1. The client gets back a `401 Unauthorized` with a `WWW-Authenticate` header pointing at the OAuth discovery URL.
2. The client registers itself (via CIMD or Dynamic Client Registration) and opens the Cometly authorization page in your browser.
3. You log in, pick which Space the client is allowed to access, and click **Approve**.
4. The client exchanges the resulting authorization code for an access token bound to that Space.
Subsequent tool calls carry that token as a bearer credential. The token is scoped with `mcp_space:{id}`, so it can only act on the Space you approved, and your Space membership and permissions are checked on every request. Removing a user from a Space immediately revokes their MCP access, with no token cleanup needed.
For **headless use cases** (CI jobs, scripts, backend services) where there's no browser, you can generate a personal access token from **Settings → Integrations → MCP** and send it as a bearer header directly, with no OAuth flow required.
See [Authentication](./authentication) for the full flow, including how to generate a token and the token limits.
## Audit log
Every tool call is recorded in an immutable `mcp_audit_log` row with:
* `correlation_id` — surfaced in error envelopes so support can trace failures
* `tool_name`, `args` (digest only, no PII), `latency_ms`, `output_size_bytes`
* `space_id`, `user_id`, `token_id`, `client_user_agent`, `client_ip`
* `was_internal` flag to distinguish external MCP clients from in-app Ask AI
Auditing is on by default for every workspace — no setup needed.
## Custom fields
Tools that touch profile data — `list_contacts`, `get_contact`, `list_events`, `query_ad_metrics` — automatically inject your Space's configured custom field labels into their input schema descriptions, so the model sees `profile_field_5 — "Customer Plan" (text)` instead of just `profile_field_5`. You can also **filter** by custom field values directly: see [`list_contacts`](/mcp/tools/list-contacts) and [`list_events`](/mcp/tools/list-events) for the `custom_field_filters` parameter.
## Where to next
OAuth 2.1 for browser-based clients, personal access tokens for server-side agents, scopes, and revoking access.
Install Cometly as an MCP server in Cursor in 30 seconds.
Install in Claude Desktop's MCP settings.
Every tool, every parameter, every operator.
Subscribable metric catalogs, dimension lists, and workspace context.
One-click analysis templates like weekly reviews and attribution comparisons.
# Prompts Overview
Source: https://docs.cometly.com/mcp/prompts/overview
Reusable prompt templates clients surface as slash-commands — pre-filled multi-step analyses.
MCP **Prompts** are templates the client can offer to users as one-click slash-commands. Each prompt embeds a multi-step instruction the agent follows, calling the right tools in the right order.
## Available prompts
| Name | Description | Arguments |
| - | - | - |
| `analyze_performance` | Analyze ad performance for a date range across one or more sources, focused on a goal. | `date_range` (required), `sources`, `goal` |
| `compare_attribution_models` | Show the same KPIs across multiple attribution models. | `date_range` (required), `metrics` (required) |
| `weekly_review` | One-shot dashboard-style summary for the most recent complete week vs the prior week. | None |
| `find_underperformers` | Surface ads / adsets / campaigns above a CPA threshold or below a ROAS threshold. | `date_range` (required), `cpa_threshold`, `roas_threshold` |
## How they work
Each prompt expands into a structured instruction that drives the agent through a series of tool calls. For example, `weekly_review` resolves to:
```
Generate a weekly performance review.
Steps:
1. Call resolve_date_range with `preset: last_week`.
2. Call query_ad_metrics_compare with `compare_to: previous_period`.
3. Call query_metric_breakdown grouped by source.
4. Write a Key Metrics + Insights + Recommendations summary suitable for a Monday-morning Slack post.
```
The agent then executes those steps and writes the final summary.
## Invoking a prompt
Via MCP `prompts/get`:
```json theme={null}
{
"method": "prompts/get",
"params": {
"name": "analyze_performance",
"arguments": {
"date_range": "last 30 days",
"sources": "facebook_ads,google_ads",
"goal": "decrease CPA"
}
}
}
```
The returned `messages` array is the multi-turn template the agent receives.
## Cost
Prompts themselves are free — they're text templates. The tool calls the agent runs as a result of the prompt are subject to the usual rate limits and (for `analyze_report`) AI credit deduction.
# Rate Limits & Errors
Source: https://docs.cometly.com/mcp/rate-limits-and-errors
Per-Space rate limits on the Cometly MCP endpoint, the structured `[error_class]` envelope, and how connected AI clients recover from failures.
## Rate limits
The MCP endpoint sits behind a per-Space throttle, shared across all MCP clients connected to that Space.
| Limit | Value |
| - | - |
| Requests per Space per minute | **60** |
That's enough headroom for typical Ask-AI-style flows where each user message triggers one to a handful of tool calls. If you're driving the MCP server from an automated agent that fans out across many tool calls per turn, you may hit the limit during a heavy iteration.
Limits are returned in standard response headers:
```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1734567890
```
When you exceed the limit you get a `429 Too Many Requests` JSON-RPC error and a `Retry-After` header. MCP-aware clients (Claude Desktop, Cursor) back off automatically on `429` and retry once the window clears.
## Underlying API limits
Each MCP tool delegates to a [Public API V1](/api-reference/endpoint/list-events) endpoint, and those endpoints have their own per-Space throttles:
| Tool | Underlying endpoint limit |
| - | - |
| `list_events`, `get_event` | 7/minute |
| `list_contacts`, `get_contact` | 15/minute |
| `list_companies`, `get_company` | 15/minute |
| `query_ad_metrics` | 30/minute |
The MCP endpoint throttle (60/minute) is hit first in most cases, but a chatty single tool can still trip its own per-endpoint limit before the MCP throttle catches it.
## Error envelope
Every error from an MCP tool returns a structured envelope so the model can recover deterministically without parsing prose:
```
[]
Reference ID:
```
The `[error_class]` prefix is one of:
| Class | When it fires | What the model should do |
| - | - | - |
| `validation_error` | Bad parameter shape, enum, or type. | Re-call with corrected args (e.g. valid date format, supported operator). |
| `authorization_error` | Token doesn't grant the requested scope, or user lost Space access. | Stop. Surface "I lost access to this Space" to the user. |
| `not_found` | Record (event/contact/company by ID) doesn't exist. | Surface "no record with that ID" to the user. Don't retry. |
| `rate_limited` | Per-Space or per-endpoint throttle hit. | Wait, back off, then retry. |
| `upstream_unavailable` | SingleStore or an ad-platform API is temporarily down. | Retry once after a short wait. |
| `internal_error` | Unhandled exception. Already logged to Sentry with the `correlation_id`. | Stop. Surface "something went wrong" to the user. |
| `unknown_tool` | Tool name doesn't exist on this server. | Drop the call; the model probably hallucinated the tool. |
The `Reference ID: …` is the row ID in `mcp_audit_log` for this call. Include it in support requests so the Cometly team can trace the failure end-to-end.
## Validation error detail
Validation errors include the field-by-field reasons, formatted for the model:
```
[validation_error] The given data was invalid.
- custom_field_filters.0.field: The custom field profile_field_19 is not configured for this space. Configure a label in the Cometly dashboard before filtering by it.
- custom_field_filters.0.operator: The operator "greater_than" is not supported for text fields. Allowed: equal_to, not_equal_to, contains, not_contains, starts_with, ends_with, contains_word, any, unknown.
Reference ID: 5a8d1e29-...
```
Validation errors are **not** reported to Sentry — they're user-input failures. Other 5xx-mapped failures are.
## Best practices for client builders
1. **Always parse the `[error_class]` prefix**, not the message text. Messages may evolve; the class won't.
2. **Honour `Retry-After`** on `429`. Cometly doesn't bill for retries, but a hot loop will keep you locked out.
3. **Surface the `Reference ID`** to end users when an error escapes. It's the fastest way to get a real diagnosis from support.
4. **Don't cache `401` responses.** When OAuth tokens expire, the next call has to walk the flow.
# Resources Overview
Source: https://docs.cometly.com/mcp/resources/overview
Subscribable read-only data the MCP server exposes alongside tools — workspace context, metric catalogs, dimension lists.
MCP **Resources** are read-only documents your client can subscribe to once and re-read on demand without burning tool-call budget. Cometly exposes five resources covering workspace orientation and the analytics vocabulary.
## At a glance
| URI | Description | Mime |
| - | - | - |
| `cometly://workspace/context` | Same payload as [`get_workspace_context`](/mcp/tools/get-workspace-context) — timezone, currency, connected ad accounts, custom event labels. | `application/json` |
| `cometly://catalog/metrics` | Every supported analytics metric slug + category + label. | `text/csv` |
| `cometly://catalog/attribution-models` | Every attribution model with a one-line description. | `application/json` |
| `cometly://catalog/dimensions` | Every `group_by` dimension across analytics tools. | `application/json` |
| `cometly://catalog/custom-events` | Live custom-event slot ↔ label map for this space. | `text/csv` |
## When to use a Resource vs a tool
Resources are best for **static-ish reference data** the agent shouldn't re-fetch every turn:
* Metric catalogs change rarely — fetch once per session.
* Attribution model definitions never change.
* Dimension lists never change.
* Custom event labels change when the user re-labels a slot — re-fetch occasionally.
For dynamic data (current campaign performance, recent webhooks, etc.) use the corresponding tool.
## Subscribing
OAuth-aware clients (Claude Desktop, Cursor) handle Resource subscription automatically. Resource reads are subject to the same `mcp_space:{id}` ability as tools.
## Example — reading a Resource
The MCP `resources/read` method returns the resource body in the same format the corresponding tool would produce:
```json theme={null}
{
"method": "resources/read",
"params": {
"uri": "cometly://catalog/custom-events"
}
}
```
Response (truncated):
```csv theme={null}
slot,label,include_in_gross_revenue
custom_event_2,Deal Created,true
custom_event_5,Lead Magnet,false
```
# analyze_report
Source: https://docs.cometly.com/mcp/tools/analyze-report
Runs Cometly's tuned report-analysis pipeline against a saved report and returns the AI-generated markdown summary.
Different from [`run_saved_report`](/mcp/tools/run-saved-report): that returns raw rows. **This** runs the same AI summarization pipeline that powers Cometly's scheduled digests and returns the tuned narrative analysis.
## Important — credit cost
This tool **deducts AI credits from the team**. It uses the same accounting as the Notification Subscription digest pipeline. Don't call repeatedly with no user intent — one call per ask.
## When to use
* "Summarize my Q3 dashboard."
* "What's the headline from last week's report?"
## When not to use
* For ad-hoc analytics — use `query_ad_metrics` and analyze in your own reasoning.
* When the team is out of credits — check via [`get_workspace_context`](/mcp/tools/get-workspace-context).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `report_id` | integer | Yes | |
## Output
Plain markdown text. Empty / error response when the team has zero credits or the provider is unavailable — pair with `get_workspace_context.ai_credit_balance` before calling.
# debug_data_flow
Source: https://docs.cometly.com/mcp/tools/debug-data-flow
Trace why a specific event isn't appearing — composes conversion counts + recent webhook activity + integration freshness.
Composes data from the conversions table + recent webhook logs + integration freshness so the agent can answer "why didn't I see `purchase` events this weekend?".
## When to use
* "No purchase events from Shopify in the last 7 days — what's wrong?"
* "Why is my Facebook spend missing for last Tuesday?"
For overall tracking health, use [`get_space_health`](/mcp/tools/get-space-health). For a specific page's pixel check, use [`inspect_site`](/mcp/tools/inspect-site) + [`verify_pixel_events`](/mcp/tools/verify-pixel-events).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `event_name` | string | Yes | The event to trace. |
| `start_date` | string | Yes | `YYYY-MM-DD HH:MM:SS` in the space timezone. |
| `end_date` | string | Yes | `YYYY-MM-DD HH:MM:SS` in the space timezone. |
| `source` | string | No | Narrow to one integration / source slug. |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
```json theme={null}
{
"event_name": "purchase",
"range": {
"start_utc": "2026-05-08T04:00:00Z",
"end_utc": "2026-05-15T03:59:59Z"
},
"conversion_count": 0,
"recent_webhooks": [
{ "id": 78421, "source": "shopify", "status": "success", "created_at": "2026-05-09T10:12:00Z" }
]
}
```
`recent_webhooks` carries the last 10 webhook receipts on the source over the requested window — use them as triage signals. For full per-fire detail (including PII-redacted payloads), follow up with [`get_webhook_event`](/mcp/tools/get-webhook-event).
`# hint:` footer surfaces the most likely root cause first ("Webhooks fired but no conversions recorded — possible event mapping issue.").
# get_ad_entity
Source: https://docs.cometly.com/mcp/tools/get-ad-entity
Full detail for a single campaign, adset, or ad — creative, targeting, delivery, budget.
Returns every column Cometly has cached for the entity, including platform-specific creative and targeting snapshots. Replaces the legacy "dump the entire ads dataset into the prompt" pattern.
## When to use
* "Show me the full config for campaign 1100012345 on Facebook."
* "What targeting / creative is this ad using?"
For tabular lists, use [`list_campaigns`](/mcp/tools/list-campaigns), [`list_adsets`](/mcp/tools/list-adsets), or [`list_ads`](/mcp/tools/list-ads).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `source` | string | Yes | `facebook_ads`, `google_ads`, `tiktok_ads`, or `linkedin_ads`. |
| `level` | string | Yes | `campaign`, `adset`, or `ad`. |
| `id` | string | Yes | The platform-side or Cometly id. |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
JSON object containing every column for the requested entity. Shape varies per platform / level. For Facebook ads, expect fields like `objective`, `daily_budget`, `targeting`, `creative_id`, `creative_body`, `effective_status`. For Google ads, expect `advertising_channel_type`, `bidding_strategy_type`, `final_url`, etc.
## Caveats
* LinkedIn ad sets don't have a separate "adset" notion in Cometly's model — only campaigns and creatives. `level: adset` for LinkedIn returns a `[validation_error]`.
* Reddit ads only support `level: ad`.
# get_chart_spec_template
Source: https://docs.cometly.com/mcp/tools/get-chart-spec-template
Chart.js v4 templates the agent populates with values from query_ad_metrics.
Returns a populated Chart.js v4 config skeleton for a given chart type. Saves the agent from reinventing the config shape — which is where most malformed-chart errors come from.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `chart_type` | string | Yes | `line`, `bar`, `pie`, `doughnut`, `radar`, or `stacked_bar`. |
## Output
```json theme={null}
{
"type": "bar",
"data": {
"labels": ["", ""],
"datasets": [{ "label": "", "data": ["", ""] }]
},
"options": {
"responsive": true,
"plugins": { "legend": { "position": "top" }, "title": { "display": true, "text": "" } },
"scales": { "y": { "beginAtZero": true } }
}
}
```
The agent populates `data.labels` and `data.datasets[].data` from the analytics tool result, then renders inside Ask AI's chart block.
# get_company
Source: https://docs.cometly.com/mcp/tools/get-company
Retrieve detail for a single company by ID.
Returns the record for one company tracked in your Space: `id`, `domain`, `external_id`, and `name`. `domain` is `null` for companies identified only by a customer-supplied `external_id` rather than an email domain; `external_id` is `null` when the customer never supplied one. A company may carry either identifier or both.
## When to use
* Drilling into a single company after [`list_companies`](/mcp/tools/list-companies) returns its ID.
* *"Tell me about company 1041."*
* *"What's the domain on company 1039?"*
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `id` | integer | Yes | The numeric ID of the company. |
## Example response
```json theme={null}
{
"id": 1041,
"domain": "acme.com",
"external_id": null,
"name": "Acme Corp"
}
```
## Caveats
* Returns 404 (`[not_found]`) if no company with that ID exists in the current Space.
* **`external_id` can be `null`** — for a company identified only by an email domain rather than a customer-supplied external ID.
* For per-contact data within a company, use [`list_contacts`](/mcp/tools/list-contacts) or pivot through [`query_ad_metrics`](/mcp/tools/query-ad-metrics) with `query_mode: "company"`.
# get_company_journey
Source: https://docs.cometly.com/mcp/tools/get-company-journey
Full chronological timeline for a single company — touchpoints + conversions across all contacts.
Same shape as `get_contact_journey` but rolled up to the company. Includes a `contact_roster` of every profile that touched ads on the company's behalf.
## When to use
* "Show me the journey for Acme Corp."
* "Which contacts at this company touched our ads?"
For per-contact journeys, use [`get_contact_journey`](/mcp/tools/get-contact-journey). For the basic company record, use [`get_company`](/mcp/tools/get-company).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `company_id` | integer | Yes | Numeric company id. |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
```json theme={null}
{
"company_id": 901,
"domain": "acme.com",
"ltv": 12849.20,
"contact_roster": [8231, 8240, 8312],
"event_count": 34,
"events": [
{ "type": "touchpoint", "event_time_utc": "2026-03-02T...", "source": "linkedin_ads", "profile_id": 8231 },
{ "type": "conversion", "event_time_utc": "2026-03-15T...", "event_name": "purchase", "amount": 4200, "profile_id": 8240 }
]
}
```
`companies` in this codebase are domain-based only — there's no human "company name" column. Surface the domain to the user.
## Caveats
Capped at 250 touchpoints + 250 conversions per company (500 events total).
# get_contact
Source: https://docs.cometly.com/mcp/tools/get-contact
Retrieve full detail for a single contact (profile) by ID, including journey, browsing data, and tracking tokens.
Returns the complete record for one contact: all emails, names, phones, location, custom profile fields, and optionally the full conversion + touchpoint journey or raw browsing session data.
## When to use
* *"Show me Jane's journey."*
* *"What pages did contact 12345 visit?"*
* *"Tell me about `jane@example.com`."* (after using [`list_contacts`](/mcp/tools/list-contacts) to find the ID).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `id` | integer | Yes | The numeric ID of the contact. |
| `include_comet_tokens` | boolean | No | Adds tracking tokens (last 5, including aliases from merged profiles). |
| `include_events` | boolean | No | Adds the contact's full event journey — all conversions and touchpoints sorted by most-recent first. |
| `hide_direct_touchpoints` | boolean | No | When `include_events` is true, hides direct-traffic touchpoints from the events array. Defaults to true. Set to false if the user explicitly wants every touchpoint. |
| `use_custom_field_labels` | boolean | No | Replace raw `profile_field_N` keys with user-defined labels in the `custom_fields` object. Always set to true unless the user wants raw column names. |
| `include_browsing_session_data` | boolean | No | Adds up to the last 1000 raw page-view hits, most-recent first. Use only when the user asks about browsing or page-level behavior. |
## Custom fields
The tool's `use_custom_field_labels` description lists every custom field configured on your Space (slug, label, type) so the model can reference them by their user-facing label in its response while still knowing the underlying slug.
```json theme={null}
{
"id": 41902,
"emails": ["jane@example.com", "jane.doe+new@example.com"],
"names": ["Jane Doe"],
"phones": [],
"location": "Austin, TX, US",
"custom_fields": {
"Customer Plan": "Premium",
"Customer Age": 34,
"Signup Date": "2025-09-12"
}
}
```
## Journey
When `include_events: true`, the response gains an `events` array 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: true` (default) drops `source: direct` touchpoints, which are usually noise. Pass `false` to include them.
## Browsing session data
When `include_browsing_session_data: true`, the response gains a `browsing_session_hits` array with up to 1000 raw page views — each hit includes `event_url`, `referrer_host`, `source`, `country`, `device_type`, `browser`, `os`, `event_name`, and `event_time_utc`.
This is heavy data. Only request it when the user explicitly asks about browsing or page-level behavior.
## Caveats
* Returns 404 (`[not_found]`) if no contact with that ID exists in the current Space.
* **All emails/names/phones are deduplicated** across the contact and any merged profiles.
* **`include_events` and `include_browsing_session_data` are independent.** Both can be set in the same request.
# get_contact_journey
Source: https://docs.cometly.com/mcp/tools/get-contact-journey
Full chronological timeline for a single contact — touchpoints interleaved with conversions.
Returns every touchpoint and conversion for a contact, sorted by time, with an LTV total. Use for "how did this lead get here?" questions or for diagnosing attribution decisions on a real example.
## When to use
* "Show me the journey for contact 8231."
* "Why was this credited to Facebook?"
* Understanding the full first-touch → conversion flow for a real customer.
For aggregated questions about many contacts, use [`query_contacts_aggregated`](/mcp/tools/query-contacts-aggregated). For the contact's basic record, use [`get_contact`](/mcp/tools/get-contact).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `profile_id` | integer | Yes | Numeric profile id. |
| `hide_direct_touchpoints` | boolean | No | Suppress direct-source touchpoints after the first. Default: `true`. |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
```json theme={null}
{
"profile_id": 8231,
"name": "Taylor Swift",
"first_active_utc": "2026-04-12T13:08:00Z",
"last_active_utc": "2026-05-14T19:02:00Z",
"ltv": 429.50,
"event_count": 18,
"events": [
{ "type": "touchpoint", "event_time_utc": "2026-04-12T13:08:00Z", "source": "facebook_ads", "ad_id": "120203" },
{ "type": "touchpoint", "event_time_utc": "2026-04-15T09:42:00Z", "source": "google_ads", "ad_id": "8423001" },
{ "type": "conversion", "event_time_utc": "2026-04-15T09:55:00Z", "event_name": "lead_generated", "amount": null },
{ "type": "conversion", "event_time_utc": "2026-05-14T19:02:00Z", "event_name": "purchase", "amount": 429.50 }
]
}
```
## Caveats
Capped at 250 touchpoints + 250 conversions. For contacts with extreme journey lengths, narrow the question rather than asking for the full timeline.
# get_dashboard
Source: https://docs.cometly.com/mcp/tools/get-dashboard
Layout and contained reports for one dashboard.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `dashboard_id` | integer | Yes | |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
```json theme={null}
{
"id": 12,
"name": "Weekly Performance",
"layout": { "grid": [...] },
"master_filters": [...],
"reports": [
{ "id": 42, "name": "ROAS by Source", "report_type": "table", "metrics": ["roas"], "sources": ["facebook_ads", "google_ads"], "attribution_model": "last_touch", "group_by": ["sources"] }
]
}
```
For full report config, follow up with [`get_report_definition`](/mcp/tools/get-report-definition). To execute a report, use [`run_saved_report`](/mcp/tools/run-saved-report).
# get_event
Source: https://docs.cometly.com/mcp/tools/get-event
Retrieve full detail for a single conversion event by ID.
Returns the complete record for one conversion event, including metadata, touchpoint attribution, and computed fields like `event_url`.
## When to use
* Drilling into a single event after [`list_events`](/mcp/tools/list-events) returns its ID.
* *"What's the full detail for event 8123?"*
* *"Show me the URL and order details for that purchase."*
For bulk listing, use [`list_events`](/mcp/tools/list-events). For aggregated metrics, use [`query_ad_metrics`](/mcp/tools/query-ad-metrics).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `id` | integer | Yes | The numeric ID of the event (returned in `list_events` rows). |
## Example response
```json theme={null}
{
"id": 8123,
"event_name": "purchase",
"configured_name": "Purchase",
"amount": 89.50,
"event_time_utc": "2026-05-13T14:22:10Z",
"event_url": "https://acme.com/checkout/confirmation?order=99812",
"order_id": "99812",
"order_name": "Spring Sale — Order #99812",
"profile_id": 41902,
"company_id": null,
"is_upsell": false,
"touchpoints": [
{
"source": "facebook_ads",
"ad_name": "Spring Sale — Carousel A",
"adset_name": "Lookalike 1% — US",
"campaign_name": "Spring 2026 — Acquisition",
"account_name": "Acme Ad Account",
"event_time_utc": "2026-05-13T13:58:22Z"
}
]
}
```
## Caveats
* Returns 404 (`[not_found]`) if no event with that ID exists in the current Space. The model is told not to retry on `not_found`.
* The `configured_name` field returns the user-defined label for custom events (`custom_event_9` → "Demo Submission") and equals `event_name` for standard events.
# get_pixel_status
Source: https://docs.cometly.com/mcp/tools/get-pixel-status
Per-domain pixel + CAPI + form-tracking installation health.
Returns each connected domain's tracking status. Pair with [`inspect_site`](/mcp/tools/inspect-site) for in-the-moment crawl-based verification.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
```json theme={null}
{
"domains": [
{
"domain": "acme.com",
"status": "verified",
"pixel_verified": true,
"capi_status": "active",
"form_tracking": "active"
},
{
"domain": "shop.acme.com",
"status": "pending",
"pixel_verified": false,
"capi_status": null,
"form_tracking": null
}
]
}
```
# get_report_definition
Source: https://docs.cometly.com/mcp/tools/get-report-definition
Full saved-report config — metrics, filters, attribution, group_by, sources, options.
Returns the report's raw row from `custom_reports` — useful for explaining what a saved report measures without executing it.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `report_id` | integer | Yes | |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
Full JSON object containing every column of the report row, including `date_range`, `attribution_model`, `attribution_window`, `metrics`, `sources`, `filters`, `options`, `group_by`, etc.
For execution, use [`run_saved_report`](/mcp/tools/run-saved-report). For the AI-summarized narrative, use [`analyze_report`](/mcp/tools/analyze-report).
# get_segment
Source: https://docs.cometly.com/mcp/tools/get-segment
Single segment's definition (conditions, last refresh, size).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `segment_id` | integer | Yes | |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
Full row from `contact_segments` including the criteria JSON, last refresh timestamp, and member count.
Returns `[not_found]` when the segment doesn't exist or segment infrastructure isn't enabled.
# get_space_health
Source: https://docs.cometly.com/mcp/tools/get-space-health
Composite tracking-health diagnostic — pixel, UTM coverage, form tracking, ad cache sync, CAPI readiness.
Cometly's "is everything tracking correctly?" check. Wraps the same engine that powers the in-product health dashboard. Returns a composite score plus per-check status.
## When to use
* "Why isn't my data showing up?"
* "Is tracking working?"
* Before recommending optimization actions — confirm data integrity first.
* As a follow-up when other tools return suspicious all-zero data.
For a single domain's pixel install check, use [`inspect_site`](/mcp/tools/inspect-site) + [`verify_pixel_events`](/mcp/tools/verify-pixel-events).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
JSON object with the full diagnostic — overall score, per-check status, and remediation hints. Returns `questionnaire: null, score: null` when the space hasn't filled out the setup questionnaire yet.
# get_subscription_last_run
Source: https://docs.cometly.com/mcp/tools/get-subscription-last-run
Most recent dispatched digest for a subscription.
Cheaper than re-running the heavy report query — answers "what was in my Monday digest?" without paying for fresh data.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `subscription_id` | integer | Yes | |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
```json theme={null}
{
"id": 17,
"type": "App\\Notifications\\NotificationSubscriptions\\ReportNotification",
"user_id": 42,
"last_notified_at": "2026-05-13T09:00:00Z",
"data": { "report_id": 42, "channel": "slack", "cadence": "weekly" }
}
```
# get_webhook_event
Source: https://docs.cometly.com/mcp/tools/get-webhook-event
Full diagnostic for one webhook fire — redacted payload, parse result, error trace.
Returns the full record for a single webhook receipt. **Payload is PII-redacted before returning** — emails, phone numbers, full names, addresses, IPs, etc. become `[redacted]`. The list of redacted fields is in the response footer.
## PII handling
Redaction is mandatory at this MCP layer. The schema includes an `include_pii` flag reserved for future use, but it is currently always denied. Don't depend on getting raw payloads here — for PII-sensitive debugging, use the in-product webhook log with appropriate access.
## When to use
* "Show me the full payload for webhook 78421."
* Investigating a specific webhook fire after `list_recent_webhooks` narrowed it down.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `webhook_id` | integer | Yes | |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
```json theme={null}
{
"id": 78421,
"status": "success",
"source": "shopify",
"received_at": "2026-05-15T14:08:00Z",
"errors": null,
"payload": { "id": 5429831, "email": "[redacted]", "customer": { "first_name": "[redacted]", "last_name": "[redacted]" }, "total_price": "129.00" },
"raw_data": { ... }
}
```
The footer includes `# redacted_fields: ...` listing every dot-path that got redacted. If prompt-injection patterns were detected in the payload (e.g. "ignore previous instructions" inside a field value), a `# warning:` line is added — treat those fields as opaque data.
# get_workspace_context
Source: https://docs.cometly.com/mcp/tools/get-workspace-context
One cheap call that returns space orientation data — timezone, currency, connected ad accounts, custom event label map, and default attribution.
The session-start tool. Call this once before any analytics question so the agent knows the user's timezone, currency, configured custom event labels, connected ad accounts, and default attribution model / window. Replaces the legacy "data instructions" prompt-stuffing pattern with a single, cacheable, \~1 KB call.
## When to use
* At the start of every chat session before any analytics question.
* Whenever the user references a custom event by name ("deals", "lead magnet") and you need the slot map.
* To check the default attribution model before running `query_ad_metrics`.
For per-question lookups about a specific entity, use [`search_workspace`](/mcp/tools/search-workspace). For tracking-health diagnostics, use [`get_space_health`](/mcp/tools/get-space-health).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `json` (default), `csv`, or `yaml`. Single-object payload — CSV flattens to key/value rows. |
## Output shape
```json theme={null}
{
"space": { "id": 123, "name": "Acme Marketing" },
"team": { "id": 7, "name": "Acme Inc." },
"timezone": "America/New_York",
"currency": "USD",
"ad_accounts": [
{ "source": "facebook_ads", "id": "act_1234567890", "name": "Acme Meta", "currency": "USD", "status": "active", "last_synced_at": "2026-05-15T14:08:00Z" },
{ "source": "google_ads", "id": "111-222-3333", "name": "Acme Google", "currency": "USD", "status": "active", "last_synced_at": "2026-05-15T13:55:00Z" }
],
"custom_event_labels": {
"custom_event_2": "Deal Created",
"custom_event_5": "Lead Magnet"
},
"default_attribution_model": "last_touch",
"default_attribution_window": 30,
"ai_credit_balance": 14200
}
```
`ai_credit_balance` reflects the **team's** `ai_credits` column (shared across every space on the team). `default_attribution_model` / `default_attribution_window` surface Cometly's product defaults — they aren't currently persisted per-space.
## Caveats
* The `ai_credit_balance` field reflects the team-level balance at request time. Don't recommend [`analyze_report`](/mcp/tools/analyze-report) (or other credit-charging tools) when the balance is at zero.
* `last_synced_at` is best-effort — different ad platforms expose different timestamp columns. Use [`get_space_health`](/mcp/tools/get-space-health) for the canonical freshness check.
# inspect_site
Source: https://docs.cometly.com/mcp/tools/inspect-site
Server-side fetch + static analysis of a customer page — pixel installation, scripts, forms, iframes.
Crawls a URL on a connected domain and reports what's actually on the page: whether the Cometly pixel is installed (and matches THIS space's uid), every other ad/analytics/marketing script, forms (native + 3rd-party widgets like HubSpot / Typeform / Calendly), iframes, framework / page-builder fingerprints.
## When to use
* The user is troubleshooting tracking, attribution, conversion events, or forms.
* You want to ground recommendations in "what's actually on this landing page" rather than assumptions.
* During the Ask AI "Troubleshoot setup" guided flow.
## Limitations — important
This is a server-side HTTP fetch + static analysis. It can confirm a script TAG is on the page, NOT that the script actually fires in a real browser. Pair with [`verify_pixel_events`](/mcp/tools/verify-pixel-events) to confirm events are actually arriving from real user traffic.
The diagnostic matrix:
* Pixel tag missing → install issue.
* Pixel tag present + 0 recent events → ad-blocker, CSP, wrong-uid, or page never visited.
* Pixel tag present + recent events → working.
## Domain scope
For safety, this tool only accepts URLs whose host matches one of THIS space's allowed domains. Off-domain URLs return `[authorization_error]` with the allowed domain list.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `url` | string | Yes | Absolute HTTPS URL on one of the space's allowed domains. |
## Output
Structured JSON with `pixel`, `scripts`, `forms`, `iframes`, `frameworks`, `meta_tags` blocks. Lead with the most decision-relevant facts in your response (pixel presence + uid match), then summarize the rest.
# list_ad_accounts
Source: https://docs.cometly.com/mcp/tools/list-ad-accounts
Every connected ad account across all platforms with currency, status, and last sync timestamp.
Returns every ad account connected to this space across Facebook, Google, LinkedIn, TikTok, Microsoft / Bing, Reddit, X, Taboola, and Whop.
## When to use
* "Which ad accounts are connected?"
* Before recommending a `sources` filter — confirm the account exists.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `sources` | string\[] | No | Restrict to specific source slugs (`facebook_ads`, `google_ads`, etc.). |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
source,id,name,currency,status,last_synced_at
facebook_ads,act_1234567890,Acme Meta,USD,active,2026-05-15T14:08:00Z
google_ads,111-222-3333,Acme Google,USD,active,2026-05-15T13:55:00Z
tiktok_ads,7234012789,Acme TikTok,USD,active,2026-05-15T14:11:00Z
```
`status` is the platform-reported account status (or Cometly's `analytics_last_synced_at` derivation when the platform doesn't expose one).
# list_ads
Source: https://docs.cometly.com/mcp/tools/list-ads
Individual ads / creatives across connected ad platforms.
Returns the ad-level entities (Meta ads, Google ads, TikTok ads, LinkedIn creatives, Reddit ads, X ads, Whop ads).
## When to use
* "List ads in this adset."
* "Which creatives are currently active?"
For full creative + targeting detail on one ad, use [`get_ad_entity`](/mcp/tools/get-ad-entity).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `sources` | string\[] | No | Source slugs to include. |
| `adset_id` | string | No | Limit to a single adset / ad group. |
| `name_like` | string | No | Case-insensitive substring filter on name. |
| `status` | string | No | Filter by status. |
| `limit` | integer | No | Max rows (1–500, default 200). |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
source,id,adset_id,name,status,updated_at
facebook_ads,1300012345,1200012345,Spring Hero Image v3,active,2026-05-15T...
google_ads,1300098765,1200098765,Brand RSA,active,2026-05-15T...
```
# list_adsets
Source: https://docs.cometly.com/mcp/tools/list-adsets
Ad sets / ad groups across connected ad platforms with status and budget.
Returns adsets (Meta), ad groups (Google, TikTok) in a unified shape.
## When to use
* "List adsets in the Spring Launch campaign."
* "Which adsets are paused?"
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `sources` | string\[] | No | Source slugs to include. |
| `campaign_id` | string | No | Limit to a single campaign. |
| `name_like` | string | No | Case-insensitive substring filter on name. |
| `status` | string | No | Filter by status. |
| `limit` | integer | No | Max rows (1–500, default 200). |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
source,id,campaign_id,name,status,budget,updated_at
facebook_ads,1200012345,1100012345,US 25-44 Lookalike,active,50.00,2026-05-15T...
google_ads,1200098765,1100098765,Brand Exact,active,100.00,2026-05-15T...
```
# list_campaigns
Source: https://docs.cometly.com/mcp/tools/list-campaigns
Campaigns across connected ad platforms with status, objective, and budget snapshot.
Returns a unified campaign list across Facebook, Google, LinkedIn, and TikTok.
## When to use
* "List my active campaigns."
* "Which campaigns are paused?"
For full detail on one campaign (creative, targeting, delivery), use [`get_ad_entity`](/mcp/tools/get-ad-entity). For ranked performance rollups, use [`query_metric_breakdown`](/mcp/tools/query-metric-breakdown).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `sources` | string\[] | No | Source slugs to include. |
| `status` | string | No | Filter by status (`active`, `paused`, …). |
| `name_like` | string | No | Case-insensitive substring filter on name. |
| `limit` | integer | No | Max rows total (1–500, default 200). Divided across the included sources. |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
source,id,account_id,name,status,objective,budget,updated_at
facebook_ads,1100012345,act_1234567890,Spring Launch,active,CONVERSIONS,150.00,2026-05-15T...
google_ads,1100098765,111-222-3333,Acme Brand Search,active,SEARCH,200.00,2026-05-15T...
```
# list_companies
Source: https://docs.cometly.com/mcp/tools/list-companies
List companies tracked in your Space. Returns one row per company with id, domain, and external_id.
Returns a cursor-paginated list of companies tracked in your Space. Useful for high-level rollups of which organizations are in your workspace. Each row includes `id`, `domain`, and `external_id`. `domain` is `null` for companies identified only by a customer-supplied `external_id` rather than an email domain; `external_id` is `null` when the customer never supplied one.
## When to use
* *"What companies do we have?"*
* *"List companies added this month."*
* *"How many distinct companies signed up last week?"*
For per-person questions, use [`list_contacts`](/mcp/tools/list-contacts). For ad performance, use [`query_ad_metrics`](/mcp/tools/query-ad-metrics).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `start_date` | string | Conditional | Optional. If set, `end_date` is required. `YYYY-MM-DD HH:MM:SS`, interpreted in your Space's timezone. Filters by company creation date. |
| `end_date` | string | Conditional | Required if `start_date` is set. |
| `per_page` | integer | No | 1–5000. Default 200. |
| `cursor` | string | No | Pagination cursor from a prior response's `next_cursor`. |
## Example response
```json theme={null}
{
"data": [
{ "id": 1041, "domain": "acme.com", "external_id": null },
{ "id": 1040, "domain": "globex.com", "external_id": null },
{ "id": 1039, "domain": null, "external_id": "acct_8f3k2" }
],
"next_cursor": "eyJpZCI6MTAzOX0",
"prev_cursor": null,
"per_page": 200
}
```
## Caveats
* **Companies don't have custom fields** — they're derived from contact email domain or external ID. There's no `custom_field_filters` parameter on this tool today.
* **`domain` can be `null`** — for a company identified only by a customer-supplied `external_id` rather than an email domain. Refer to those companies by their `external_id`, or by ID if that's null too.
* **`external_id` can be `null`** — for a company identified only by an email domain. It's `null` unless the customer explicitly set one via [Update Company](/api-reference/endpoint/update-company) or events carrying `company_external_id`.
* **`name` is not included** in the list view — use [`get_company`](/mcp/tools/get-company) to retrieve it.
* **Order is newest first** by `id` descending.
# list_contacts
Source: https://docs.cometly.com/mcp/tools/list-contacts
List contacts (profiles) created within a date range, or look them up by exact email address, with optional custom-field include and value filtering.
Returns one row per contact (profile) created in the date range, or matching an exact email address, with primary email, name, phone, and location. Optionally enriched with all known emails, Cometly tracking tokens, or the full set of custom profile fields.
## When to use
* *"Who signed up last week?"*
* *"List leads from this month."*
* *"Show me new contacts created today."*
* *"Find contacts where Customer Plan is Premium."*
* *"Do we have a contact for [jane@acme.com](mailto:jane@acme.com)?"* → pass `email`, no date range needed.
For a single contact's full journey, use [`get_contact`](/mcp/tools/get-contact). For ad-performance questions, use [`query_ad_metrics`](/mcp/tools/query-ad-metrics). Searching for a specific named person by name should go through `search_workspace` first — but an exact email address is better served by this tool's `email` parameter.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `start_date` | string | Conditional | `YYYY-MM-DD HH:MM:SS`, interpreted in your Space's timezone. Filters by contact creation date. Required unless `email` is supplied, and must be sent as a pair with `end_date`. |
| `end_date` | string | Conditional | `YYYY-MM-DD HH:MM:SS`, must be after `start_date`. Required unless `email` is supplied, and must be sent as a pair with `start_date`. |
| `email` | string\[] | No | Look contacts up by exact email address instead of (or alongside) the date range. Matching is exact and case-insensitive. Matches both the contact's primary address and any of its alternate addresses, including addresses captured on profiles later merged into the contact. Up to 100 addresses per call. A contact matched on an alternate address comes back with its *primary* in `email`, not the address you queried — set `include_all_emails` when passing more than one address, or you cannot tell which input produced which row. |
| `per_page` | integer | No | 1–5000. Default 200. |
| `cursor` | string | No | Pagination cursor from a prior response's `next_cursor`. |
| `include_all_emails` | boolean | No | Adds an array of all known email addresses per contact (including merged profiles). |
| `include_comet_tokens` | boolean | No | Adds the contact's last 5 Cometly tracking tokens. |
| `include_custom_fields` | boolean | No | Includes the 30 custom profile fields (`profile_field_1` through `profile_field_30`). |
| `use_custom_field_labels` | boolean | No | When `include_custom_fields` is true, replaces raw column names with user-defined labels (e.g. `"Customer Age"`). Always set this to true unless the user explicitly wants raw column names. |
| `custom_field_filters` | array | No | AND-joined conditions on custom profile fields. See below. |
## Custom field filters
Flat array of AND-joined conditions referencing slots configured on your Space:
```json theme={null}
[
{ "field": "profile_field_5", "operator": "equal_to", "value": "Premium" },
{ "field": "profile_field_18", "operator": "greater_than", "value": 30 },
{ "field": "profile_field_27", "operator": "between", "start": "2025-01-01", "end": "2025-12-31" }
]
```
The tool's input schema lists the **labels and types your Space has configured** for slots 1–30, so the model picks the right slug automatically when you mention a user-facing label like "Customer Plan" or "Customer Age." Slots that haven't been configured are rejected with `validation_error`. Maximum 10 conditions per request.
### Operator vocabulary by field type
| Slot range | Type | Operators |
| - | - | - |
| `profile_field_1` – `profile_field_15` | text | `equal_to`, `not_equal_to`, `contains`, `not_contains`, `starts_with`, `ends_with`, `contains_word`, `any`, `unknown` |
| `profile_field_16` – `profile_field_25` | number | `equal_to`, `not_equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `any`, `unknown` |
| `profile_field_26` – `profile_field_30` | date | `equal_to`, `between`, `greater_than`, `less_than`, `age_greater_than`, `age_equal_to`, `age_less_than`, `any`, `unknown` |
`any` matches contacts where the field has a value; `unknown` matches contacts where the field is empty or unset. For numeric fields, `any` requires a value greater than `0` and `unknown` matches `0` or unset. Date comparisons (`equal_to`, `greater_than`, `less_than`, `between`) use your space's configured timezone; `equal_to` matches the full calendar day.
## Example response
```json theme={null}
{
"data": [
{
"id": 41902,
"email": "jane@example.com",
"name": "Jane Doe",
"phone": null,
"location": "Austin, TX, US",
"Customer Plan": "Premium",
"Customer Age": 34
}
],
"next_cursor": "eyJpZCI6NDE5MDJ9",
"prev_cursor": null,
"per_page": 200
}
```
When `use_custom_field_labels` is true, the response keys carry the labels you configured (`"Customer Plan"`, `"Customer Age"`); otherwise they appear as raw `profile_field_N` keys.
## Caveats
* **Primary data only.** The list endpoint returns one email, name, phone, and location per contact. For all known values across merged profiles, use [`get_contact`](/mcp/tools/get-contact) or pass `include_all_emails: true`.
* **Order is newest first** by `id` descending. Cursor pagination uses `id` as the cursor key.
* **Empty filter slots are rejected.** Filtering on `profile_field_19` when slot 19 has no label returns a validation error rather than silently returning zero rows.
* **`not_equal_to` / `not_contains` exclude unset values.** Contacts where the field is unset are not included. Add a separate `unknown` condition if you need to capture those.
* **`email` is an array only.** Unlike the HTTP endpoint's query string, the MCP tool's input schema doesn't accept a comma-separated string for `email` — always pass an array of addresses, even for a single lookup.
* **No match is not an error.** An `email` (or date range) with no matching contacts returns an empty `data` array, not a validation error.
# list_custom_events
Source: https://docs.cometly.com/mcp/tools/list-custom-events
List this space's configured custom conversion event slots with their human labels.
Cometly stores custom conversion events in 50 generic slots (`custom_event_1` … `custom_event_50`). Each space configures human labels for the slots it uses. This tool returns the label map so the agent can resolve a user phrase like "deals" → `custom_event_2` without guessing.
## When to use
* The user references an event by its human name and you need to pass the slot id to [`query_ad_metrics`](/mcp/tools/query-ad-metrics) or [`list_events`](/mcp/tools/list-events).
* You're answering "what custom events does this space track?".
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
slot,label,include_in_gross_revenue
custom_event_1,Lead Magnet,true
custom_event_2,Deal Created,false
custom_event_5,Webinar Registration,true
```
Only configured slots appear — empty `custom_event_*` slots are omitted by default.
## Tip — label resolution works either way
You don't always need to call this tool first. The label resolver accepts either form on analytics tools:
```json theme={null}
{ "metrics": ["custom_event_2"] }
```
```json theme={null}
{ "metrics": ["Deal Created"] }
```
Both work. Calling `list_custom_events` is still cheaper than a `[validation_error]` round-trip on a misspelled label, though.
## Related
* [`get_workspace_context`](/mcp/tools/get-workspace-context) embeds the same map for session-start use.
* The `cometly://catalog/custom-events` MCP Resource serves the same data — OAuth-aware clients can subscribe and avoid re-fetching.
# list_dashboards
Source: https://docs.cometly.com/mcp/tools/list-dashboards
Every dashboard (custom report grid) in this space.
## When to use
* "What dashboards do we have?"
* Before [`get_dashboard`](/mcp/tools/get-dashboard) when the user doesn't know the id.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
id,name,report_count,updated_at
12,Weekly Performance,8,2026-05-12T10:14:00Z
17,Q1 Snapshot,12,2026-04-01T18:00:00Z
```
# list_event_mappings
Source: https://docs.cometly.com/mcp/tools/list-event-mappings
How external events from each integration map to Cometly events.
Surfaces the rules that translate platform events (Shopify `orders/paid`, Stripe `charge.succeeded`, HubSpot deal stage transitions, etc.) into Cometly's internal event names.
## When to use
* "Why did Shopify's `orders/paid` become a `purchase` in Cometly?"
* Auditing event mapping configuration before recommending integration changes.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
source,external_event,cometly_event,cometly_event_label
shopify,orders/paid,purchase,
hubspot,deal.created,custom_event_2,Deal Created
google_ads,Lead Form Submission,lead_generated,
stripe,charge.succeeded,purchase,
```
`cometly_event_label` is the human label when the mapping targets a `custom_event_*` slot.
# list_event_types
Source: https://docs.cometly.com/mcp/tools/list-event-types
Distinct event names in this space with first/last seen dates and a count over a lookback window.
Returns the empirical list of conversion event names the space has actually received — distinct from [`list_custom_events`](/mcp/tools/list-custom-events) which lists *configured* slots (some of which may be empty).
## When to use
* "What events am I tracking?"
* Before composing a `query_events_aggregated` call when you're not sure which events have data.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `lookback_days` | integer | No | How many days of history to scan (1–365, default 30). |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
event_name,label,first_seen,last_seen,count
purchase,purchase,2025-08-12 14:22:00,2026-05-14 23:51:00,28411
custom_event_2,Deal Created,2025-09-04 09:13:00,2026-05-14 19:02:00,892
lead_generated,lead_generated,2025-08-12 14:22:00,2026-05-14 23:51:00,4124
```
`label` is the configured human-readable name for `custom_event_*` slots; otherwise it's the canonical event slug.
# list_events
Source: https://docs.cometly.com/mcp/tools/list-events
List individual conversion events with optional attribution, field selection, and custom-field-value filtering.
Returns one row per conversion event in a date range. Use this when the user wants to see *individual* events. For aggregated questions (totals, ROAS, trends), prefer [`query_ad_metrics`](/mcp/tools/query-ad-metrics).
## When to use
* *"Show me yesterday's purchases."*
* *"List sign-ups from Google Ads last week."*
* *"What were the most recent leads attributed to Facebook?"*
* *"List purchases from contacts where Customer Plan is Premium this month."*
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `start_date` | string | Yes | `YYYY-MM-DD HH:MM:SS`, interpreted in your Space's timezone. |
| `end_date` | string | Yes | `YYYY-MM-DD HH:MM:SS`, must be after `start_date`. |
| `query_mode` | string | No | `profile` (default) or `company`. |
| `event_names` | string\[] | No | Filter by event names. The model passes user-facing custom event labels here ("Demo Submission") and the server translates to the internal slug (`custom_event_9`). |
| `sources` | string\[] | No | Filter by traffic sources. When set, `attribution_model` and `attribution_window` are required. |
| `attribution_model` | string | Conditional | Required when `sources` is set. |
| `attribution_window` | integer | Conditional | Required when `sources` is set. `0` = LTV. |
| `fields` | string\[] | No | Which fields to return per event. Default: `["id", "event_time_utc"]`. Touchpoint fields (`ad_id`, `ad_name`, `adset_id`, `adset_name`, `campaign_id`, `campaign_name`, `account_id`, `account_name`) require `sources`. |
| `per_page` | integer | No | 1–4000. Default 200. |
| `cursor` | string | No | Pagination cursor from a prior response's `next_cursor`. |
| `custom_field_filters` | array | No | AND-joined conditions on the owning contact's custom profile fields. See **Custom field filters** below. |
## Custom field filters
`custom_field_filters` is a flat array of conditions, ANDed together. Each condition references a custom profile field configured on your Space:
```json theme={null}
[
{ "field": "profile_field_5", "operator": "equal_to", "value": "Premium" },
{ "field": "profile_field_18", "operator": "greater_than", "value": 30 },
{ "field": "profile_field_27", "operator": "between", "start": "2025-01-01", "end": "2025-12-31" }
]
```
The tool's input schema lists the **labels and types your Space has configured** for slots 1–30, so the model picks the right slug automatically when you ask about "Customer Plan" or "Customer Age." Slots that haven't been configured with a label are rejected with a `validation_error`.
### Operator vocabulary by field type
| Slot range | Type | Operators |
| - | - | - |
| `profile_field_1` – `profile_field_15` | text | `equal_to`, `not_equal_to`, `contains`, `not_contains`, `starts_with`, `ends_with`, `contains_word`, `any`, `unknown` |
| `profile_field_16` – `profile_field_25` | number | `equal_to`, `not_equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `any`, `unknown` |
| `profile_field_26` – `profile_field_30` | date | `equal_to`, `between`, `greater_than`, `less_than`, `age_greater_than`, `age_equal_to`, `age_less_than`, `any`, `unknown` |
`any` matches contacts where the field has a value; `unknown` matches contacts where the field is empty or unset. For numeric fields, `any` requires a value greater than `0` and `unknown` matches `0` or unset. Date comparisons (`equal_to`, `greater_than`, `less_than`, `between`) use your space's configured timezone; `equal_to` matches the full calendar day.
## Attribution behavior
Without `sources`, you get one row per event in the date range — no attribution.
With `sources`, multi-touch attribution applies and **each event may return multiple rows** — one per attributed touchpoint. A $100 conversion attributed across 4 touchpoints via `linear` returns 4 rows, each crediting $25. Single-touch models (`first_touch`, `last_touch`) return one row per event.
## Example response
```json theme={null}
{
"data": [
{
"id": "8123",
"event_time_utc": "2026-05-13T14:22:10Z",
"event_name": "purchase",
"amount": 89.50,
"ad_name": "Spring Sale — Carousel A",
"campaign_name": "Spring 2026 — Acquisition",
"source": "facebook_ads"
}
],
"next_cursor": "eyJpZCI6ODEyMywiZXZlbnRfdGltZV91dGMiOiIyMDI2LTA1LTEzVDE0OjIyOjEwWiJ9",
"prev_cursor": null,
"per_page": 200
}
```
## Caveats
* **Default fields** are minimal (`id`, `event_time_utc`) to keep responses fast. Ask for more via `fields` when you actually need them.
* **Touchpoint fields require sources.** Asking for `ad_name` without setting `sources` returns a validation error.
* **`event_url`** is computed from `metadata.host + path + query` — useful for "what page did this fire from?" questions.
* **`not_equal_to` / `not_contains` exclude unset values.** Contacts where the field is unset are not included. Add a separate `unknown` condition if you need to capture those.
# list_filterable_fields
Source: https://docs.cometly.com/mcp/tools/list-filterable-fields
Every field that can appear inside a filters object — slug, type, allowed operators, and labels for profile fields.
Enumerates every field accepted by the `filters` parameter on `query_ad_metrics`, `list_events`, and `list_contacts`. For each field, returns the type and the operators that type allows.
## When to use
* Before authoring a `filters` object when you're not certain of the slug or which operator is valid for the type.
* Avoids the `[validation_error]` round-trip from `ReportDataValidator`.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
| `include_unused` | boolean | No | Include `profile_field_*` slots without a configured label (default `false`). |
| `category` | string | No | Narrow to one category: `contact`, `company`, `ad`, `conversion`, `profile_field`, `all`. Default `all`. |
## Output
```csv theme={null}
slug,type,operators,label
email,string,equal_to|not_equal_to|starts_with|ends_with|contains|not_contains|contains_word|any|unknown,
country,string,equal_to|not_equal_to|starts_with|ends_with|contains|not_contains|contains_word|any|unknown,
purchase,numeric,equal_to|not_equal_to|greater_than|greater_than_or_equal_to|less_than|less_than_or_equal_to|any|unknown,
profile_field_1,string,equal_to|not_equal_to|...,Customer Plan
profile_field_18,numeric,equal_to|not_equal_to|greater_than|...,Customer Age
profile_field_27,date,equal_to|between|greater_than|...,Signup Date
```
## Tip
Custom profile fields (`profile_field_*`) are typed by slot range:
* `profile_field_1` – `profile_field_15` → text
* `profile_field_16` – `profile_field_25` → number
* `profile_field_26` – `profile_field_30` → date
The default scope hides profile-field slots without a configured label so the agent isn't tempted to filter on slots that hold no data.
# list_integrations
Source: https://docs.cometly.com/mcp/tools/list-integrations
Every connected integration in this space — OAuth (HubSpot, Shopify, Slack, ...) and inbound webhooks.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
provider,kind,name,status,last_active_at
hubspot,oauth,Acme HubSpot,connected,2026-05-15T13:48:00Z
shopify,oauth,acme.myshopify.com,connected,2026-05-15T14:00:00Z
slack,oauth,Acme Workspace,connected,2026-05-15T11:22:00Z
shopify,webhook,Shopify orders/paid → purchase,enabled,2026-05-15T14:08:00Z
```
* `kind: oauth` rows are OAuth-managed integrations (HubSpot, Shopify, Slack, …).
* `kind: webhook` rows are inbound webhook subscriptions (from the `webhooks` table). The `name` column carries the description or event name; `status` is `enabled` / `disabled`.
For per-webhook receipt history use [`list_recent_webhooks`](/mcp/tools/list-recent-webhooks). For event-mapping rules use [`list_event_mappings`](/mcp/tools/list-event-mappings).
# list_metric_catalog
Source: https://docs.cometly.com/mcp/tools/list-metric-catalog
Every analytics metric slug supported by query_ad_metrics et al, scoped to what this space actually uses.
Enumerates every metric the analytics tools accept, with a short type tag and (for `custom_event_*` metrics) the configured human label. Call this before `query_ad_metrics` whenever you're unsure of the canonical slug — saves a `[validation_error]` round-trip on misspellings.
## When to use
* Before composing a `query_ad_metrics` / `query_metric_breakdown` / `query_ad_metrics_compare` call when you're not certain a metric slug exists.
* To enumerate everything the user can ask for ("what metrics are available?").
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
| `include_unused` | boolean | No | Include `custom_event_*` slots without a configured label (default `false`). |
## Output
```csv theme={null}
slug,category,label
amount_spent,ad,
purchase,conversion,
purchase_amount,conversion_amount,
roas,ad,
cost_per_purchase,cost_per_conversion,
custom_event_2,custom_event,Deal Created
cost_per_custom_event_2,cost_per_custom_event,Deal Created
custom_event_2_amount,custom_event_amount,Deal Created
```
`category` is one of: `analytics`, `ad`, `meta`, `tiktok`, `linkedin`, `microsoft`, `conversion`, `cost_per_conversion`, `conversion_amount`, `conversion_roas`, `custom_event`, `cost_per_custom_event`, `custom_event_amount`, `custom_event_roas`.
## Related
* The `cometly://catalog/metrics` MCP Resource serves the same data.
* For user-defined metric formulas, see [`list_user_defined_metrics`](/mcp/tools/list-user-defined-metrics).
# list_my_spaces
Source: https://docs.cometly.com/mcp/tools/list-my-spaces
Every space the authenticated user can access, with a marker on the one bound to this MCP session.
Lists every space the current user belongs to and identifies which one this MCP session is authorized for. Useful for agency / multi-client workflows where the user juggles many workspaces.
## When to use
* "What spaces / teams / clients do I have access to?"
* "Am I in the right space?"
## Session scope
MCP tokens are pinned to **one space** at mint time (the `mcp_space:{id}` ability). This tool tells you which other spaces the user *belongs to* but you cannot read data from them in the current session — they'd need a separate MCP connection per space (see the in-app MCP settings page).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
id,name,team,current
123,Acme Marketing,Acme Inc.,true
456,Beta Co.,Acme Inc.,false
789,Client A,Agency LLC,false
```
# list_recent_webhooks
Source: https://docs.cometly.com/mcp/tools/list-recent-webhooks
Recent webhook receipts with status, source, and summarized payload — payloads are PII-summarized, not raw.
Returns recent webhook fires. Payloads are summarized to keys + types only — no PII bytes appear in this tool's output. For a single webhook's full (redacted) payload, use [`get_webhook_event`](/mcp/tools/get-webhook-event).
## When to use
* "Did Stripe fire any webhooks today?"
* "Why are we seeing errors from the HubSpot integration?"
* Debugging an integration without exposing customer PII.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `status` | string | No | One of `pending_processing`, `processing`, `processed`, `error`. |
| `source` | string | No | Integration provider slug (e.g. `shopify`, `stripe`). |
| `limit` | integer | No | 1–200, default 50. |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
id,status,source,received_at,error_summary,payload_summary
78421,processed,shopify,2026-05-15T14:08:00Z,,"id,customer,line_items,total_price"
78422,error,stripe,2026-05-15T14:09:00Z,Signature verification failed,
```
`status` reflects Cometly's processing lifecycle (`pending_processing` → `processing` → `processed` | `error`).
`payload_summary` is the comma-separated keys of the payload (top-level), capped at 10.
`error_summary` is the first error message truncated to 200 chars.
# list_reports
Source: https://docs.cometly.com/mcp/tools/list-reports
Saved reports across this space, optionally filtered to one dashboard.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `dashboard_id` | integer | No | Limit to reports inside this dashboard. |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
id,name,dashboard_id,dashboard_name,report_type,metrics,sources,attribution_model
42,ROAS by Source,12,Weekly Performance,table,roas|purchase,facebook_ads|google_ads,last_touch
43,Daily Spend,12,Weekly Performance,line_chart,amount_spent,facebook_ads|google_ads,last_touch
```
`metrics` and `sources` are pipe-delimited inside the cell.
# list_segments
Source: https://docs.cometly.com/mcp/tools/list-segments
Contact segments defined in this space.
Lists saved audience segments. Note: segment infrastructure isn't enabled on every space — the tool returns an empty result with a hint when the underlying table is absent.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
id,name,description,size,updated_at
12,High-Intent Leads,Visited pricing page 3+ times in 30 days,1240,2026-05-12T...
17,Inactive 90d,No engagement in 90 days,8420,2026-05-12T...
```
# list_subscriptions
Source: https://docs.cometly.com/mcp/tools/list-subscriptions
Scheduled report digest subscriptions for this space.
Lists recurring AI-summarized reports the space has configured (delivered to Slack / email on a cadence).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
id,type,user_id,cadence,last_notified_at,data
17,App\Notifications\NotificationSubscriptions\ReportNotification,42,weekly,2026-05-13T09:00:00Z,{"report_id":42,"channel":"slack"}
```
`data` is the serialized subscription config — which report, recipients, channel preferences.
# list_user_defined_metrics
Source: https://docs.cometly.com/mcp/tools/list-user-defined-metrics
User-defined custom metric formulas configured in the report builder.
Lists every User-Defined Metric the space has set up — formulas the user composed in the report builder, like `Profit Margin = (gross_revenue - amount_spent) / gross_revenue`. These behave like first-class metric slugs in `query_ad_metrics`.
## When to use
* The user references a metric name that isn't in the standard catalog ("show my LTV efficiency metric"). Confirm the slug exists here before passing it to an analytics tool.
* To enumerate every space-defined calculation as part of an overview.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
id,slug,name,type,expression
12,profit_margin,Profit Margin,percent,( gross_revenue - amount_spent ) / gross_revenue
17,ltv_efficiency,LTV Efficiency,number,ltv / amount_spent
```
`type` is one of `number`, `currency`, or `percent`. `expression` is a human-readable rendering of the formula tokens (order matters for the calculation).
# Tools Overview
Source: https://docs.cometly.com/mcp/tools/overview
Every MCP tool Cometly exposes, organized by toolset, with when-to-use guidance.
The Cometly MCP server exposes **50+ tools** across 14 toolsets covering workspace context, analytics, journeys, ads-manager hierarchy, reports, segments, integrations, health, diagnostics, webhook debugging, and AI helpers. All but three are read-only; the write tools (`update_contact`, `update_company`, [`update_event`](/mcp/tools/update-event)) edit fields on one record by ID and are marked **(write)** below. This page documents the **public MCP surface** — what Cursor, Claude Desktop, ChatGPT, and other external clients see after OAuth. The in-app Ask AI chat gets the same tools plus additional internal-only capabilities (see below).
Bulk extraction lives in the [Public API V1 export endpoints](/api-reference/endpoint/create-event-export) and the in-app CSV export buttons — not in the MCP. Hand the dashboard URL to the user, or call the V1 export endpoints directly, instead of routing 50k rows through an LLM.
## At a glance
### Workspace
| Tool | Best for |
| - | - |
| [`get_workspace_context`](/mcp/tools/get-workspace-context) | One cheap call that returns space name, timezone, currency, connected ad accounts, custom event labels, and default attribution. Call this at session start. |
| [`list_custom_events`](/mcp/tools/list-custom-events) | Resolve "deals" → `custom_event_2` so the agent never guesses slot ids. |
| [`list_my_spaces`](/mcp/tools/list-my-spaces) | Show every space the authenticated user can access, with a marker on the current one. |
| [`list_metric_catalog`](/mcp/tools/list-metric-catalog) | Enumerate every supported analytics metric slug, scoped to what this space actually uses. |
| [`list_filterable_fields`](/mcp/tools/list-filterable-fields) | List every field that can appear in a `filters` object, with allowed operators per type. |
| [`list_user_defined_metrics`](/mcp/tools/list-user-defined-metrics) | List space-defined custom metric formulas (built in the report builder). |
| [`resolve_date_range`](/mcp/tools/resolve-date-range) | Turn "last week" / "this month" / "Q1 2026" into concrete UTC bounds. Prevents off-by-one errors at DST boundaries. |
### Search
| Tool | Best for |
| - | - |
| [`search_workspace`](/mcp/tools/search-workspace) | Fuzzy search across campaigns, ad sets, ads, contacts, companies, reports, dashboards, segments. |
### Analytics
| Tool | Best for |
| - | - |
| [`query_ad_metrics`](/mcp/tools/query-ad-metrics) | Primary tool for aggregated ad performance. |
| [`query_ad_metrics_compare`](/mcp/tools/query-ad-metrics-compare) | Two-period comparison (current vs previous) in one call. |
| [`query_attribution_models`](/mcp/tools/query-attribution-models) | Same query across multiple attribution models. |
| [`query_metric_breakdown`](/mcp/tools/query-metric-breakdown) | "Top N by X" ranked drill-down. |
### Events
| Tool | Best for |
| - | - |
| [`list_events`](/mcp/tools/list-events) | Individual conversion events. |
| [`get_event`](/mcp/tools/get-event) | Single event detail. |
| [`update_event`](/mcp/tools/update-event) **(write)** | Correct the `amount` on one existing event without touching its contact, journey position, or attribution. |
| [`query_events_aggregated`](/mcp/tools/query-events-aggregated) | Counted / summed event totals bucketed by time + source. |
| [`list_event_types`](/mcp/tools/list-event-types) | Distinct event names + first/last seen + counts. |
### Contacts
| Tool | Best for |
| - | - |
| [`list_contacts`](/mcp/tools/list-contacts) | Cursor-paginated contact list with custom-field filtering. |
| [`get_contact`](/mcp/tools/get-contact) | Single contact detail. |
| `update_contact` **(write)** | Replace a contact's emails, phones, names, locations, or custom fields (sparse PUT — any collection you send is the full authoritative list). |
| [`get_contact_journey`](/mcp/tools/get-contact-journey) | Full chronological timeline (touchpoints + conversions) for one contact. |
| [`query_contacts_aggregated`](/mcp/tools/query-contacts-aggregated) | "How many contacts converted from X last month?" aggregated rollups. |
### Companies
| Tool | Best for |
| - | - |
| [`list_companies`](/mcp/tools/list-companies) | Company-level rollup. |
| [`get_company`](/mcp/tools/get-company) | Single company detail. |
| `update_company` **(write)** | Change a company's `domain`, `external_id`, or `name`. Identifier changes are rejected when another company already owns the value. |
| [`get_company_journey`](/mcp/tools/get-company-journey) | Full company-wide timeline with contact roster. |
### Ads Manager
| Tool | Best for |
| - | - |
| [`list_ad_accounts`](/mcp/tools/list-ad-accounts) | Every connected ad account with currency + last sync. |
| [`list_campaigns`](/mcp/tools/list-campaigns) | Campaign roster across platforms. |
| [`list_adsets`](/mcp/tools/list-adsets) | Ad sets / ad groups. |
| [`list_ads`](/mcp/tools/list-ads) | Individual ads / creatives. |
| [`get_ad_entity`](/mcp/tools/get-ad-entity) | Full campaign/adset/ad detail. |
### Reports
| Tool | Best for |
| - | - |
| [`list_dashboards`](/mcp/tools/list-dashboards) | Dashboards in this space. |
| [`get_dashboard`](/mcp/tools/get-dashboard) | One dashboard + its reports. |
| [`list_reports`](/mcp/tools/list-reports) | Saved reports across the space. |
| [`get_report_definition`](/mcp/tools/get-report-definition) | A saved report's full config. |
| [`run_saved_report`](/mcp/tools/run-saved-report) | Execute a saved report as configured. |
| [`analyze_report`](/mcp/tools/analyze-report) | Tuned AI narrative summary (deducts AI credits). |
| [`list_subscriptions`](/mcp/tools/list-subscriptions) | Scheduled digest subscriptions. |
| [`get_subscription_last_run`](/mcp/tools/get-subscription-last-run) | Most recent dispatched digest for a subscription. |
### Segments
| Tool | Best for |
| - | - |
| [`list_segments`](/mcp/tools/list-segments) | Contact segments defined in the space. |
| [`get_segment`](/mcp/tools/get-segment) | One segment's definition. |
| [`query_segment_membership`](/mcp/tools/query-segment-membership) | Paginated members of a segment. |
### Integrations
| Tool | Best for |
| - | - |
| [`list_integrations`](/mcp/tools/list-integrations) | Every connected integration (OAuth + webhook). |
| [`get_pixel_status`](/mcp/tools/get-pixel-status) | Per-domain pixel + CAPI + form-tracking health. |
| [`list_event_mappings`](/mcp/tools/list-event-mappings) | How external integration events map to Cometly events. |
### Health
| Tool | Best for |
| - | - |
| [`get_space_health`](/mcp/tools/get-space-health) | Composite tracking-health checklist. |
| [`debug_data_flow`](/mcp/tools/debug-data-flow) | Trace why a specific event isn't appearing. |
### Diagnostics
| Tool | Best for |
| - | - |
| [`inspect_site`](/mcp/tools/inspect-site) | Server-side crawl of a landing page for tracking installation. |
| [`verify_pixel_events`](/mcp/tools/verify-pixel-events) | Confirm pixel events are actually arriving from a domain. |
### Webhooks
| Tool | Best for |
| - | - |
| [`list_recent_webhooks`](/mcp/tools/list-recent-webhooks) | Recent webhook receipts with status + summarized payload. PII redacted. |
| [`get_webhook_event`](/mcp/tools/get-webhook-event) | One webhook fire with redacted payload + parse result. |
### AI helpers
| Tool | Best for |
| - | - |
| [`search_cometly_docs`](/mcp/tools/search-cometly-docs) | Grep over Cometly product docs for "how does X work?" questions. |
| [`get_chart_spec_template`](/mcp/tools/get-chart-spec-template) | Chart.js v4 templates the agent fills in. |
The in-app Ask AI chat also gets tools that are **not** on the public MCP transport:
| Tool | Why it's internal |
| - | - |
| `list_recent_chats`, `get_chat_room`, `search_chat_history` | Only meaningful inside Cometly's chat UI. |
| `read_attachment` | Reads files uploaded to an Ask AI thread. |
| `query_cohort_analysis` | Cohort retention tables — Ask AI only for now. |
| `remember_fact`, `forget_fact`, `search_memory` | Persistent memory for Ask AI — registered separately, never exposed externally. |
These are marked **internal-only** (`external: false`) or live only in the in-app agent registry. External MCP clients (Cursor, Claude Desktop, ChatGPT) won't see them in `tools/list`.
## Tool naming convention
| Prefix | Meaning |
| - | - |
| `list_*` | Returns a cursor-paginated array. Tools accept a `cursor` parameter to walk the result set. |
| `get_*` | Returns a single record by ID. |
| `query_*` | Aggregated analytics — returns a result set shaped by `metrics`, `group_by`, and `filters`. |
| `search_*` | Fuzzy search (lower-precision lookup helpers). |
| `resolve_*` | Pure helpers that turn human-readable input into structured output. |
| `debug_*` | Diagnostic, multi-source composition. |
| `analyze_*` | Runs Cometly's AI pipelines — deducts credits. |
## Output format
Most tools accept `format: "csv" | "json" | "yaml"`. **CSV is the default for tools added in v2.0** because it's \~50% cheaper in tokens for tabular results. The legacy tools (`list_events`, `list_contacts`, `query_ad_metrics`, etc.) keep `json` as the default for backwards compatibility — pass `format: "csv"` explicitly when you want compactness.
Token-budget pagination kicks in around \~6,000 tokens per tool response; when truncation happens the response includes a `truncated_at_token_budget: true` footer and an opaque `next_cursor` you should pass back verbatim.
## Custom event awareness
Tools that touch custom events — `list_events`, `query_ad_metrics`, `query_events_aggregated`, and friends — automatically rewrite `custom_event_*` slot ids to your configured labels in their output. You can also pass labels directly:
```json theme={null}
{ "metrics": ["Deal Created", "purchase"] }
```
The server resolves "Deal Created" to its slot id (e.g. `custom_event_2`) on the way in. The result column header comes back as `Deal Created`, never the internal slug.
## Authentication & errors
All external tools authenticate via the OAuth-issued Sanctum token described in [Authentication](/mcp/authentication). Errors come back as `[error_class] message` envelopes — see [Rate Limits & Errors](/mcp/rate-limits-and-errors) for the full taxonomy.
# query_ad_metrics
Source: https://docs.cometly.com/mcp/tools/query-ad-metrics
Aggregated ad performance metrics from the Cometly report builder — ROAS, CPA, spend, conversions, with grouping, attribution, and filters.
The primary tool for any question about marketing performance — spend efficiency, ROAS/CPA, conversion volume, platform comparison. Wraps the same engine that powers Cometly's report builder and Custom Dashboards.
## When to use
* Aggregated questions: *"What's my ROAS by source last 30 days?"*, *"Daily spend trend for Facebook?"*, *"Top adsets by purchases this month?"*
* Comparisons across attribution models, windows, sources, or time periods.
* Group-by reporting on ad hierarchy, geography, device, browser, or traffic source.
For *individual* events, use [`list_events`](/mcp/tools/list-events). For per-person journeys, use [`get_contact`](/mcp/tools/get-contact).
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `date_range` | string | Yes | Preset like `last_30_days`, `this_month`, `last_quarter`, `custom`, etc. Use `custom` with `date_start` / `date_end` for arbitrary ranges. |
| `attribution_model` | string | Yes | `first_touch`, `last_touch`, `last_non_direct_touch`, `linear`, `linear_paid_ads`, `u_shaped`, `first_touch_source_specific`, `last_touch_source_specific`. |
| `attribution_window` | integer | Yes | Lookback in days: `1`, `7`, `14`, `30`, `60`, `90`, or `0` for LTV. |
| `attribution_window_type` | string | Yes | `relative` (default) or `absolute`. |
| `metrics` | string\[] | Yes | Which metrics to return. Common: `amount_spent`, `impressions`, `clicks`, `link_clicks`, `purchase`, `purchase_amount`, `roas`, `gross_revenue`, `net_profit`, `roi`, `cpm`, `cpc`, `ctr`, `cost_per_purchase`, `average_order_value`, `lead_generated`, `add_to_cart`, `view_content`, `sign_up`, plus `custom_event_1` through `custom_event_50`. |
| `date_start` | string | No | `YYYY-MM-DD`. Required when `date_range` is `custom`, `equal_to`, or `after`. |
| `date_end` | string | No | `YYYY-MM-DD`. Required when `date_range` is `custom`, `equal_to`, or `before`. |
| `query_mode` | string | No | `profile` (default) or `company`. |
| `group_by` | string\[] | No | Group results by one or more dimensions. Time: `day`, `week`, `month`, `quarter`, `year`. Ad hierarchy: `campaign_name`, `adset_name`, `ad_name`, `account_name` (and `_id` variants). Other: `sources`, `country`, `state`, `city`, `device_type`, `browser`, `os`, `url`, `referrer`, `keyword`. |
| `sources` | string\[] | No | Restrict to specific traffic sources (`facebook_ads`, `google_ads`, `tiktok_ads`, `linkedin_ads`, `bing_ads`, `reddit_ads`, `x_ads`, `taboola_ads`, `whop_ads`, etc.). |
| `filters` | object | No | Nested filter tree. See **Filters** below. |
## Filters
Same shape as Cometly's report builder. Conditions are grouped, and groups are connected by AND / OR:
```json theme={null}
{
"connector": "and",
"groups": [
{
"connector": "and",
"conditions": [
{ "filter": "campaign_name", "operator": "contains", "value": "spring sale", "start": null, "end": null },
{ "filter": "country", "operator": "equal_to", "value": "US", "start": null, "end": null },
{ "filter": "profile_field_5", "operator": "equal_to", "value": "Premium", "start": null, "end": null }
]
}
]
}
```
### Operator vocabulary
| Field type | Operators |
| - | - |
| Text | `equal_to`, `not_equal_to`, `contains`, `not_contains`, `starts_with`, `ends_with`, `contains_word`, `any`, `unknown` |
| Number | `equal_to`, `not_equal_to`, `greater_than`, `greater_than_or_equal_to`, `less_than`, `less_than_or_equal_to`, `any`, `unknown` |
| Date | `equal_to`, `between`, `greater_than`, `less_than`, `age_greater_than`, `age_equal_to`, `age_less_than`, `any`, `unknown` |
### Filterable slugs (subset)
Demographic & touchpoint: `email`, `email_domain`, `country`, `region`, `city`, `device_type`, `os`, `browser`, `language`, `referrer`, `url`, `url_parameters`, `keyword`.
Ad hierarchy: `ad_account_id`, `ad_campaign_id`, `ad_set_id`, `ad_id`, plus `_name` variants.
Conversion counts & dates: any standard event name (`purchase`, `lead_generated`, etc.) as a numeric count filter, plus `_date` and `_first_date` variants.
Custom profile fields: `profile_field_1` through `profile_field_30`. The tool description lists the **labels and types your Space has configured** so the model knows which slot to use for which user-facing label.
Lifetime value: `ltv`.
## Example response
For a request grouping `purchase`, `purchase_amount`, and `roas` by `sources` over the last 30 days, the model gets back something like:
```json theme={null}
{
"rows": [
{ "source": "facebook_ads", "purchase": 412, "purchase_amount": 38420.50, "roas": 3.82 },
{ "source": "google_ads", "purchase": 297, "purchase_amount": 27110.20, "roas": 4.15 },
{ "source": "tiktok_ads", "purchase": 88, "purchase_amount": 6210.75, "roas": 2.41 }
],
"totals": { "purchase": 797, "purchase_amount": 71741.45, "roas": 3.61 }
}
```
The model then renders this as a chart, markdown table, or whichever format you asked for.
## Caveats
* **`attribution_window: 0` means LTV** — the model is instructed to refer to it as "LTV (Lifetime Value)" rather than "0-day" in user-facing responses.
* **Reported Revenue / Reported Purchases** come from Meta's API and are platform-reported (often inaccurate). The model is instructed not to mention them unless explicitly asked.
* **Brand search campaigns:** the model is told not to recommend budget scaling on Google branded search — volume is capped by brand awareness, not budget.
# query_ad_metrics_compare
Source: https://docs.cometly.com/mcp/tools/query-ad-metrics-compare
Run the same metrics across two periods (current vs previous) in one call with side-by-side deltas.
A wrapper around `query_ad_metrics` that runs the query twice — current period and the comparison period — and returns both row sets plus a per-metric delta and percent-change. Avoids the double-round-trip of two separate calls.
## When to use
* "How did last week compare to the week before?"
* "Is CPA down vs last month?"
* WoW / MoM / YoY questions.
For comparing **attribution models** (not periods), use [`query_attribution_models`](/mcp/tools/query-attribution-models). For point-in-time queries, use [`query_ad_metrics`](/mcp/tools/query-ad-metrics).
## Parameters
Accepts every parameter `query_ad_metrics` does, plus:
| Parameter | Type | Required | Description |
| - | - | - | - |
| `compare_to` | string | No | `previous_period` (default), `previous_year`, or `custom`. |
| `previous_date_start` | string | Conditional | Required when `compare_to=custom`. `YYYY-MM-DD`. |
| `previous_date_end` | string | Conditional | Required when `compare_to=custom`. `YYYY-MM-DD`. |
Previous-period semantics:
* `this_*` presets map to their `last_*` counterparts (`this_week` → `last_week`, etc.).
* Trailing windows (`last_7_days`, `last_30_days`, …) shift the same span backward.
* Custom date ranges with `compare_to: previous_year` subtract 365 days from both bounds.
## Output
```json theme={null}
{
"current": [{ "source": "facebook_ads", "purchase": 412, "roas": 3.82 }],
"previous": [{ "source": "facebook_ads", "purchase": 387, "roas": 3.55 }],
"delta": [
{ "key": "sources=facebook_ads", "metric": "purchase", "current": 412, "previous": 387, "delta": 25, "pct_change": 6.46 },
{ "key": "sources=facebook_ads", "metric": "roas", "current": 3.82, "previous": 3.55, "delta": 0.27, "pct_change": 7.6 }
]
}
```
`pct_change` is `null` when the previous-period value is `0` (avoids divide-by-zero).
## Caveats
Attribution windows are calendar-day-based (UTC). Across DST transitions a 7-day window may span 6d 23h or 7d 1h — mention this when explaining period-over-period changes if the period crosses a DST boundary.
# query_attribution_models
Source: https://docs.cometly.com/mcp/tools/query-attribution-models
Run the same metrics query across multiple attribution models in one call.
A wrapper around `query_ad_metrics` that fans out across multiple `attribution_model` values and returns one result set per model. Answers "how does my CPA / ROAS look across attribution models?" without N round-trips.
## When to use
* "What's my CPA across attribution models?"
* "Does last\_touch agree with linear\_paid on the top campaigns?"
* Any question reasoning about attribution methodology.
For comparing **periods** (not models), use [`query_ad_metrics_compare`](/mcp/tools/query-ad-metrics-compare).
## Parameters
Accepts every parameter `query_ad_metrics` does, except `attribution_model` is replaced by:
| Parameter | Type | Required | Description |
| - | - | - | - |
| `attribution_models` | string\[] | Yes | 1–8 attribution models to query. |
## Output
```json theme={null}
{
"by_attribution_model": {
"first_touch": [{ "source": "facebook_ads", "purchase": 412, "roas": 3.82 }],
"last_touch": [{ "source": "facebook_ads", "purchase": 487, "roas": 4.51 }],
"linear": [{ "source": "facebook_ads", "purchase": 449, "roas": 4.16 }],
"u_shaped": [{ "source": "facebook_ads", "purchase": 458, "roas": 4.24 }]
}
}
```
## Tip
For brand vs prospecting analysis, contrasting `first_touch` and `last_touch` usually shows the largest spread. `linear` and `u_shaped` sit between the two extremes and are good "balanced" reference points for discussion.
# query_contacts_aggregated
Source: https://docs.cometly.com/mcp/tools/query-contacts-aggregated
Aggregated count / group-by over contacts (people), pinned to profile-mode aggregation.
Same query plumbing as `query_ad_metrics`, but pinned to `query_mode: profile` so the row is "one per contact" rather than "one per event".
## When to use
* "How many contacts converted from LinkedIn Ads last month?"
* "Count of contacts by country with at least one purchase."
* "Distribution of contacts by device type."
For per-contact data, use [`list_contacts`](/mcp/tools/list-contacts) or [`get_contact`](/mcp/tools/get-contact). For events, use [`query_events_aggregated`](/mcp/tools/query-events-aggregated).
## Parameters
Same shape as `query_ad_metrics`. `query_mode` is forced to `profile`.
## Output
CSV with one row per group combination:
```csv theme={null}
country,purchase,gross_revenue
US,412,38420.50
CA,98,8920.10
UK,73,5510.40
```
# query_events_aggregated
Source: https://docs.cometly.com/mcp/tools/query-events-aggregated
Aggregated count / sum / group-by over conversion events, without hydrating individual event rows.
A wrapper around `query_ad_metrics` that pre-filters to conversion metrics (counts + amounts) and curates the `group_by` surface to "time + source" type dimensions agents actually want here.
## When to use
* "Daily purchase count last 30 days."
* "Sum of revenue by ad source last week."
* Any question that wants a TOTAL or BUCKET, not individual rows.
For per-event rows use [`list_events`](/mcp/tools/list-events). For ad-spend / ROAS metrics use [`query_ad_metrics`](/mcp/tools/query-ad-metrics) directly.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `date_range` | string | Yes | DateRange preset. |
| `attribution_model` | string | Yes | |
| `attribution_window` | integer | Yes | |
| `attribution_window_type` | string | Yes | `relative` or `absolute`. |
| `event_names` | string\[] | Yes | Event slugs OR human labels. Both work — the LabelResolver normalizes. |
| `group_by` | string\[] | No | Bucketing dimensions. Default: `["day"]`. Valid: `day`, `week`, `month`, `quarter`, `year`, `sources`, `country`, `state`, `city`, `device_type`, `browser`, `os`, `campaign_name`, `adset_name`, `ad_name`. |
| `date_start` / `date_end` | string | Conditional | When `date_range=custom`. |
| `sources` | string\[] | No | Restrict to traffic sources. |
| `filters` | object | No | Same filter shape as `query_ad_metrics`. |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
CSV with `, , ` rows:
```csv theme={null}
day,Deal Created,Deal Created_amount,purchase,purchase_amount
2026-05-08,12,4200,87,8240
2026-05-09,15,5300,92,8830
```
`# label_map: custom_event_2=Deal Created` appears in the footer when custom events were used.
# query_metric_breakdown
Source: https://docs.cometly.com/mcp/tools/query-metric-breakdown
Top-N ranked drill-down across one or more dimensions.
Same query plumbing as `query_ad_metrics` but with built-in sort + limit semantics. Optimized for "show me top 10 campaigns by ROAS" style questions where you want a ranked list, not a flat aggregate.
## When to use
* "Top 10 campaigns by ROAS last 7 days."
* "Worst 5 adsets by CPA this month."
* Any ranked list.
For period-over-period (`query_ad_metrics_compare`), multi-attribution (`query_attribution_models`), or single-row aggregates (`query_ad_metrics`), use those tools instead.
## Parameters
Accepts every parameter `query_ad_metrics` does, plus:
| Parameter | Type | Required | Description |
| - | - | - | - |
| `group_by` | string\[] | Yes | At least one dimension to break down by. |
| `order_by` | string | No | Metric slug to rank by. Defaults to the first metric in `metrics`. |
| `order_dir` | string | No | `asc` or `desc`. Default: `desc`. |
| `limit` | integer | No | Top-N limit (1–100, default 10). |
## Output
CSV by default. Same shape as `query_ad_metrics`, with an added `_rank` column reflecting the requested ordering:
```csv theme={null}
_rank,campaign_name,amount_spent,purchase,roas
1,Spring Launch,4820.50,142,4.92
2,Brand Search,2110.30,98,4.41
3,Retargeting,1955.80,73,3.74
...
```
# query_segment_membership
Source: https://docs.cometly.com/mcp/tools/query-segment-membership
Paginated members (contacts) of a segment.
Lists the profiles that belong to a saved segment. For ad-hoc filtering by free-form conditions, use [`list_contacts`](/mcp/tools/list-contacts) with `custom_field_filters` instead.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `segment_id` | integer | Yes | |
| `per_page` | integer | No | 1–200, default 100. |
| `cursor` | string | No | Opaque pagination cursor. |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
profile_id,email,name,added_at
8231,taylor@example.com,Taylor Swift,2026-05-10T...
8240,sam@example.com,Sam Smith,2026-05-11T...
```
# resolve_date_range
Source: https://docs.cometly.com/mcp/tools/resolve-date-range
Turn a natural-language or preset date phrase into concrete start/end UTC bounds in the space's timezone.
Maps "last week" / "this month" / "yesterday" / "Q1 2026" to deterministic `{start, end, timezone, days_inclusive}` bounds based on the space's configured timezone. Prevents off-by-one errors at week / month / DST boundaries that come from the model doing the math itself.
## When to use
* Any time the user says a relative date phrase and you want a reproducible start/end you can pass into `query_ad_metrics` or `list_events`.
* Cross-platform comparisons that need the same window definition across multiple tool calls.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `preset` | string | Conditional | Exact `DateRange` preset (`last_7_days`, `this_week`, `last_month`, etc.). |
| `relative` | string | Conditional | Freeform phrase like `"last week"`, `"yesterday"`, `"this month"`. The server matches it to a preset. |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
Provide at least one of `preset` or `relative`. `preset` wins when both are present.
## Output
```json theme={null}
{
"preset": "last_7_days",
"start": "2026-05-08T00:00:00-04:00",
"end": "2026-05-14T23:59:59-04:00",
"timezone": "America/New_York",
"days_inclusive": 7,
"dst_caveat": "This window crosses a DST transition in America/New_York. The window length differs from a flat 24h * N days by 1 hour, which may slightly skew period-over-period comparisons."
}
```
The `dst_caveat` field is only included when the window crosses a DST transition in the space's timezone.
## Supported relative phrases
`today`, `yesterday`, `this week`, `last week`, `this month`, `last month`, `this quarter`, `last quarter`, `this year`, `last year`, `last 7 days`, `last 14 days`, `last 30 days`, `last 60 days`, `last 90 days`, `last 365 days`, `all time`.
Substring matches work (`"compare last week to the week before"` resolves to `last_week`). Ambiguous phrases return a `[validation_error]` with a hint listing the supported presets.
## Caveats
Attribution windows in `query_ad_metrics` are calendar-day-based (UTC), not 24-hour rolling. Across DST transitions a 7-day window may span 6d 23h or 7d 1h. The `dst_caveat` field flags this — surface it in your reply when explaining period-over-period changes that cross a DST boundary.
# run_saved_report
Source: https://docs.cometly.com/mcp/tools/run-saved-report
Execute a saved report as configured and return the dataset the dashboard renders.
Runs the report with its stored attribution model, window, filters, sources, and group\_by — exactly as the dashboard would show it. Trust the user's saved config; for ad-hoc analysis with overrides, use [`query_ad_metrics`](/mcp/tools/query-ad-metrics) directly.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `report_id` | integer | Yes | |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
```json theme={null}
{
"report_id": 42,
"name": "ROAS by Source",
"rows": [
{ "source": "facebook_ads", "roas": 3.82, "purchase": 412 },
{ "source": "google_ads", "roas": 4.15, "purchase": 297 }
]
}
```
Custom event slot ids are rewritten to user labels in column names — `# label_map:` footer carries the mapping.
## Related
* [`get_report_definition`](/mcp/tools/get-report-definition) — inspect the report without running it.
* [`analyze_report`](/mcp/tools/analyze-report) — run the report AND get an AI-generated narrative summary (deducts AI credits).
# search_cometly_docs
Source: https://docs.cometly.com/mcp/tools/search-cometly-docs
Lightweight substring search across Cometly's product documentation.
Grep-style search over the same MDX content you're reading now. Use when the user asks "how does Cometly handle X?" or "what does Y mean?" rather than guessing — the docs are the source of truth for product behavior and naming.
## When to use
* "How does Cometly define a conversion?" → search "conversion".
* "What does the linear attribution model mean?" → search "linear".
* "Can I export my contacts?" → search "export".
For *data* about THIS space, use the analytics / events / contacts tools instead.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `query` | string | Yes | Search terms (case-insensitive substring match). Minimum 2 characters. |
| `limit` | integer | No | Max results (1–20, default 5). |
| `format` | string | No | `json` (default), `csv`, or `yaml`. |
## Output
```json theme={null}
[
{
"path": "publicApiDocs/V1/mcp/tools/query-ad-metrics.mdx",
"title": "query_ad_metrics",
"snippet": "...the Cometly report builder...attribution_window: 0 means LTV — the model is instructed to refer to it as..."
}
]
```
`snippet` is a ±200-character window around the match.
# search_workspace
Source: https://docs.cometly.com/mcp/tools/search-workspace
Fuzzy search across campaigns, ad sets, ads, contacts, companies, reports, and dashboards.
One call returns matching entities across every searchable kind in the space. Use when the user mentions something by name and you need its id before calling a more specific tool.
## When to use
* *"Pull up the TikTok Q3 Launch campaign."* → search, find the id, then `get_ad_entity`.
* *"Find the dashboard called 'Weekly Performance'."* → search → `get_dashboard`.
* *"Look up contact `taylor@example.com`."* → search → `get_contact`.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `query` | string | Yes | The search needle. Case-insensitive substring match. Minimum 2 characters. |
| `kinds` | string\[] | No | Entity kinds to include: `ad_account`, `campaign`, `adset`, `ad`, `contact`, `company`, `report`, `dashboard`. Default: all. |
| `limit` | integer | No | Max rows per kind (1–25, default 5). |
| `format` | string | No | `csv` (default), `json`, or `yaml`. |
## Output
```csv theme={null}
kind,id,name,extras
ad_account,act_1234567890,Acme Meta,source=facebook_ads
campaign,1100012345,Acme Spring Launch,source=facebook_ads
campaign,1100098765,Acme Spring - Display,source=google_ads
contact,8231,Taylor Swift,email=taylor@example.com
report,42,Weekly Performance,
dashboard,12,Q1 Snapshot,
```
`extras` is kind-specific context (source for ad entities, email for contacts). Use `kind` + `id` to call the appropriate detail tool — `get_ad_entity` for ad entities, `get_contact` for contacts, `get_dashboard` for dashboards, etc.
## Caveats
This is a substring match over the canonical name column — it doesn't reach into custom field values or message bodies. For those, use the specific `list_*` tool with filters.
# update_event
Source: https://docs.cometly.com/mcp/tools/update-event
Correct the amount on an existing conversion event by its numeric ID.
Corrects the `amount` on an existing conversion event by its numeric ID. Returns a confirmation with the `id` and the `amount` that was written — not the full event record. Call [`get_event`](/mcp/tools/get-event) afterwards if you need to read the event back.
## When to use
* *"Event 123 should be $49, not $3,036."*
* *"Fix the amount on that purchase."*
* *"Clear the amount on that refunded order."* — HTTP-only, see [Caveats](#caveats).
Find the event ID first with [`list_events`](/mcp/tools/list-events), [`get_event`](/mcp/tools/get-event), or [`get_contact_journey`](/mcp/tools/get-contact-journey) if you don't have it.
Do not use this to backfill events that never happened, to move an event in time, or to change which contact it belongs to — none of those are possible here.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `id` | integer | Yes | The numeric ID of the conversion event to update. |
| `amount` | number | Yes | New amount for the event, in the Space currency. Must be `0` or greater. |
## Semantics
Sparse update: fields you pass are replaced, everything else is left as it is. `amount` is the updatable field today. The event's contact, time, name, and attribution are not changed.
* `amount: 49` → the event now carries \$49.
* `amount: 0` → the event now carries \$0 (still counted as a conversion).
## Example response
```json theme={null}
{
"message": "Event updated successfully",
"id": "8123",
"amount": 49
}
```
## Caveats
* **`amount` is required and cannot clear the amount.** Unlike the HTTP endpoint [`PUT /events/{id}`](/api-reference/endpoint/update-event), which accepts `amount: null` to clear the figure, this tool treats `null` as "not provided" — clearing an amount is only possible through the HTTP API.
* Returns a `not_found` error if no event with that ID exists in the current Space, including IDs that belong to another Space.
* The rate limit is the same 60 requests/minute write bucket as the HTTP endpoint.
# verify_pixel_events
Source: https://docs.cometly.com/mcp/tools/verify-pixel-events
Confirm pixel events are actually arriving from a domain by querying Cometly's ingestion logs.
Queries Cometly's tracker ingestion to verify the pixel is firing for real user traffic on a domain. Authoritative signal for "is the pixel actually working?" — complements [`inspect_site`](/mcp/tools/inspect-site), which only confirms the tag is present in HTML.
## When to use
* After `inspect_site` confirms the pixel tag is present but the user reports no data.
* To confirm a recent install is live before the user runs ads.
## Parameters
| Parameter | Type | Required | Description |
| - | - | - | - |
| `domain` | string | Yes | The domain to verify (must be connected to this space). |
| `lookback_hours` | integer | No | How far back to look (default 24). |
## Output
JSON with event counts bucketed by event\_name and timestamp ranges showing when the most recent fires occurred. Use this to diagnose "pixel installed but no data" → "pixel installed and last fired 18 hours ago" → "ad-blocker share is materially affecting visibility".