# Fathom Analytics API v1 > Manage sites, pull reports, and track events from your own code. - **Base URL:** `https://api.usefathom.com/v1` - **Authentication:** Bearer token. Send `Authorization: Bearer YOUR_API_TOKEN` - **Headers:** send `Accept: application/json` so errors are returned as JSON - **OpenAPI schema:** https://usefathom.com/api/openapi.json - **Human-readable reference:** https://usefathom.com/api/v1 --- ## Fathom Analytics API Use the API to manage sites, events and milestones, or pull custom reports. The base URL is `https://api.usefathom.com/v1`. Each request counts towards your monthly pageviews. Ten Create Site calls count the same as ten pageviews. That keeps the API flexible for you without us having to raise plan prices to cover it. The first time anyone runs a unique aggregation, there is a short delay while the database builds a query plan. After that, it is fast. > See our [community guidelines](/legal/guidelines) for what you can and can't do when building plugins, integrations or connections to the API. ## Rate limits and concurrency The API has two separate limits, both applied per account across every endpoint: - A **per-hour** limit on all API requests. This is the sustained-volume backstop; concurrency is the primary safeguard. - A **concurrency** limit: how many requests you can have in flight at the same time, across all endpoints. Every account includes a free allowance. Need more? Upgrade your API plan in your [billing settings](https://app.usefathom.com/settings/billing). Plans are self-serve and apply immediately: | Plan | Price | Per hour All endpoints | Concurrent All endpoints | | --- | --- | --- | --- | | Tier 1 | Included | 600 | 5 | | Tier 2 | $19/mo | 1,000 | 5 | | Tier 3 | $39/mo | 2,000 | 10 | | Tier 4 | $79/mo | 4,000 | 15 | | Tier 5 | $199/mo | 8,000 | 20 | | Tier 6 | $399/mo | 16,000 | 25 | When you exceed any limit we return a `429` with a `Retry-After` header. Wait that many seconds, then retry. ## Create a token To create a token for our API, go to [https://app.usefathom.com/api](https://app.usefathom.com/api) and click `Create new`. - Click `Create new` from the API page. - Give your token a name, and set permissions (Admin, read only (all sites), or site-specific). - Click `Save changes`. - Write down your API token and save it in a safe place. You won't be able to retrieve your token once you navigate away from that page. ## Authentication To connect to our API, you must use the `Authorization` header with a bearer token. This can be obtained from your dashboard, under Settings → API. Your API key has complete access to your account (depending on the permissions you set), so please protect it. You should also send `Accept: application/json` so that errors are returned as JSON. ## Pagination All top-level API resources allow you to fetch records in bulk via our "list" methods. These list methods share a common, cursor-based pagination structure. We've taken inspiration from the excellent Stripe API, so this will be familiar to a lot of you. You can paginate using either `starting_after` or `ending_before` (but not both at once): - When you use `starting_after`, we sort records in chronological order, allowing you to page from the start of time up until the current day. - When you use `ending_before`, we sort records in reverse chronological order, effectively working backwards from the object ID you give us. Every list response is wrapped in a list object. The `has_more` boolean tells you whether another page is available, and the value to pass as your next cursor is the `id` of the first or last item in `data`. All list endpoints accept these query string parameters: - `limit`: the number of objects to return, between 1 and 100 (default 10). - `starting_after`: an object ID that defines your place in the list, paging forwards. - `ending_before`: an object ID that defines your place in the list, paging backwards. [`GET /v1/aggregations`](/api/v1/reports#aggregation) is not a list endpoint, does not use this pagination, and has its own `limit` (default 500 when grouping, max 1000). **List response shape** ```json { "object": "list", "url": "/v1/sites", "has_more": true, "data": [ { "...": "..." } ] } ``` ## Errors The Fathom API uses conventional HTTP status codes to indicate the result of a request. Codes in the `2xx` range mean success, codes in the `4xx` range indicate a problem with the information you supplied (a missing parameter, an invalid token, a permissions issue, and so on), and codes in the `5xx` range indicate an error on our side (these are rare). Always send `Accept: application/json` so that errors are returned as JSON rather than an HTML error page. Most errors return a single `error` key with a human-readable message. Validation errors (for example, a missing required field) instead return an `errors` object keyed by field name, with an array of messages for each field. | Status | Meaning | When you'll see it | | --- | --- | --- | | `400` | Bad Request | The request was rejected. This covers invalid or missing parameters, a failed validation check (returned as an `errors` object, for example an aggregation `limit` above 1000), a token that lacks permission for the action, an unsupported `entity`, an hourly `date_grouping` over a range longer than 7 days, or an account whose subscription has lapsed. | | `401` | Unauthorized | Your API token is missing, malformed or no longer valid. Check the `Authorization: Bearer` header you're sending. | | `404` | Not Found | The site, event or milestone you referenced doesn't exist, or your token doesn't have access to it. | | `410` | Gone | The endpoint has been retired and no longer functions. This currently applies to the legacy Wipe Site and Wipe Event endpoints, and to renaming an event with the legacy Update Event endpoint. | | `422` | Unprocessable Entity | The request was understood but can't be fulfilled, typically an aggregation that combines incompatible fields or groupings. | | `429` | Too Many Requests | You've exceeded a [rate limit or your concurrency limit](/api/v1/rate-limits). Check the `Retry-After` response header and back off before retrying. | | `500` | Server Error | Something went wrong on our end. These are rare and we're alerted automatically. Retry shortly, and [contact us](mailto:support@usefathom.com) if it persists. | | `503` | Service Unavailable | Our analytics API is briefly at capacity. Check the `Retry-After` response header and retry shortly. | > **Branch on the status code:** Error messages are written for humans and may change over time. When handling errors programmatically, branch on the HTTP status code rather than matching on the message text. **General error** ```json { "error": "This token doesn't have permission to access this endpoint" } ``` **Validation error** ```json { "errors": { "name": [ "The name field is required." ] } } ``` **Unauthorized** ```json { "error": "Unauthenticated" } ``` **Rate limited** ```json { "message": "Too Many Attempts." } ``` **Concurrency limit reached** ```json { "error": { "code": "api_concurrency_limit_reached", "message": "You have too many API requests in flight at once. Your current API plan allows up to 5 concurrent requests. Wait for some to finish, or upgrade to Tier 3 for 10 concurrent requests at $39/mo. See https://usefathom.com/api/v1/rate-limits for details.", "limit": 5, "next_tier": { "name": "Tier 3", "concurrent_queries": 10, "price": "$39/mo" } } } ``` ## Issues If you run into a problem, or have an idea, [email us](mailto:support@usefathom.com). --- ## Account Information about the account and token behind your API key. ### Get account ```http GET /account ``` Retrieve information about the account that owns the API key. **Permissions:** Requires a token with full account access (the `*` scope). **Returns:** An account object. **Example request** ```bash curl "https://api.usefathom.com/v1/account" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "id": 500302232, "object": "account", "name": "Homer Simpson", "email": "homer@simpsonsfamily.doh" } ``` ### Get token ```http GET /token ``` Retrieve metadata about the API token used to make the request, including its name, permissions (abilities), token-format version and timestamps. Your secret token value is never returned. **Permissions:** Any valid API token. **Returns:** A token object. **Example request** ```bash curl "https://api.usefathom.com/v1/token" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "name": "Production server", "version": 4, "abilities": [ "*" ], "last_used_at": "2024-01-15 09:24:00", "expires_at": null, "created_at": "2024-01-01 12:00:00", "updated_at": "2024-01-15 09:24:00" } ``` --- ## Sites Create and manage the sites in your Fathom account. ### List sites ```http GET /sites ``` Return a list of all sites this API key owns. Sites are sorted by `created_at` ascending to allow you to paginate with ease. **Permissions:** Requires read access to all sites (`all-sites-readonly`) or full account access. **Returns:** A list of site objects. **Parameters** - `limit` (integer, query, optional): A limit on the number of objects to be returned, between 1 and 100. Default: `10`. - `starting_after` (string, query, optional): A cursor for use in pagination. `starting_after` is an object ID that defines your place in the list. For example, if you make a list request and receive 10 objects ending with `obj_foo`, your subsequent call can include `starting_after=obj_foo` to fetch the next page. - `ending_before` (string, query, optional): A cursor for use in pagination, working in the opposite direction to `starting_after`. `ending_before` is an object ID that defines your place in the list. **Example request** ```bash curl "https://api.usefathom.com/v1/sites" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "object": "list", "url": "/v1/sites", "has_more": false, "data": [ { "id": "CDBUGS", "object": "site", "name": "Bugs Bunny Portfolio", "sharing": "none", "created_at": "2020-07-27 12:01:01", "timezone": "America/New_York" } ] } ``` ### Get site ```http GET /sites/{site_id} ``` Return a single site. **Permissions:** Requires read access to the site (`all-sites-readonly`, `read:{site_id}` or `manage:{site_id}`). **Returns:** A site object. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` **Example request** ```bash curl "https://api.usefathom.com/v1/sites/CDBUGS" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "id": "CDBUGS", "object": "site", "name": "Bugs Bunny Portfolio", "sharing": "none", "created_at": "2020-07-27 12:01:01", "timezone": "America/New_York" } ``` ### Create site ```http POST /sites ``` Create a site. **Permissions:** Requires full account access (`*`). **Returns:** A site object. **Parameters** - `name` (string, body, required): The name of the website. Any string (up to 255 characters) is acceptable, and it doesn't have to match the website URL. Example: `Daffy's Website` - `sharing` (string, body, optional): The sharing configuration. Supported values are `none`, `private` or `public`. One of: `none`, `private`, `public`. Default: `none`. - `share_password` (string, body, Required if sharing is private): When sharing is set to `private`, you must also send a password to access the site with (up to 255 characters). - `timezone` (string, body, optional): The site's reporting timezone as a [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g. `America/New_York`). If omitted, the site inherits your account's default timezone. - `multi_domain` (boolean, body, optional): Set to `true` to allow this site to track multiple domains. Default: `false`. - `multi_domain_option` (string, body, Required if multi_domain is true): How multi-domain data is grouped. Supported values are `combined` (report all domains together) or `separate` (report each domain individually). One of: `combined`, `separate`. **Example request** ```bash curl -X POST "https://api.usefathom.com/v1/sites" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "name": "Bugs Bunny Portfolio" }' ``` **Example response (`200`)** ```json { "id": "GCDFS", "object": "site", "name": "Acme Inc", "sharing": "none", "created_at": "2021-08-10 00:45:43", "timezone": "America/New_York" } ``` ### Update site ```http POST /sites/{site_id} ``` Update a site. Send only the fields you want to change. **Permissions:** Requires write access to the site (`manage:{site_id}`). **Returns:** A site object. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `name` (string, body, optional): The name of the website (up to 255 characters). - `sharing` (string, body, optional): The sharing configuration. Supported values are `none`, `private` or `public`. One of: `none`, `private`, `public`. - `share_password` (string, body, Required if sharing is private): When sharing is set to `private`, you must also send a password to access the site with (up to 255 characters). - `timezone` (string, body, optional): The site's reporting timezone as a [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g. `America/New_York`). Reporting for this site will use this timezone. - `multi_domain` (boolean, body, optional): Set to `true` to allow this site to track multiple domains. - `multi_domain_option` (string, body, Required if multi_domain is true): How multi-domain data is grouped: `combined` or `separate`. One of: `combined`, `separate`. **Example request** ```bash curl -X POST "https://api.usefathom.com/v1/sites/CDBUGS" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "id": "GCDFS", "object": "site", "name": "Acme Holdings Inc", "sharing": "private", "created_at": "2021-08-10 00:45:43", "timezone": "America/New_York" } ``` ### Wipe site ```http DELETE /sites/{site_id}/data ``` > **Deprecated.** This endpoint has been retired and now returns `410 Gone`. It is no longer possible to wipe a site's data via the API. Previously wiped all pageviews and event completions from a website. This endpoint is no longer available. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` ### Delete site ```http DELETE /sites/{site_id} ``` Delete a site. Careful: you can't undo this, and neither can we. **Permissions:** Requires full account access (`*`). **Returns:** Returns a deleted object on success. Otherwise, this call returns an error. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` **Example request** ```bash curl -X DELETE "https://api.usefathom.com/v1/sites/CDBUGS" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "id": "GCDFS", "object": "site", "deleted": true } ``` --- ## Events Manage events for a site. ### List events ```http GET /sites/{site_id}/events ``` Return a list of this site's events: every event name the site has tracked, plus any event you've set a currency for. Events are sorted by `name`. **Permissions:** Requires read access to the site (`all-sites-readonly`, `read:{site_id}` or `manage:{site_id}`). **Returns:** A list of event objects. > Identify events by `name`. Goal IDs were removed on 24 September 2026, so don't store `id`. Only use it as the `starting_after` or `ending_before` cursor when you page through this list. > `currency` is the currency you set with [Set event currency](#set-event-currency), or `null` when the event uses your site's default. `created_at` is `null` for newer events. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `limit` (integer, query, optional): A limit on the number of objects to be returned, between 1 and 100. Default: `10`. - `starting_after` (string, query, optional): A cursor for use in pagination. `starting_after` is an object ID that defines your place in the list. For example, if you make a list request and receive 10 objects ending with `obj_foo`, your subsequent call can include `starting_after=obj_foo` to fetch the next page. - `ending_before` (string, query, optional): A cursor for use in pagination, working in the opposite direction to `starting_after`. `ending_before` is an object ID that defines your place in the list. **Example request** ```bash curl "https://api.usefathom.com/v1/sites/CDBUGS/events" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "object": "list", "url": "/v1/sites/CDBUGS/events", "has_more": false, "data": [ { "id": "IJKLMNOP", "object": "event", "name": "Purchased product", "currency": "pound", "created_at": "2020-08-21 15:24:00" }, { "id": "ABCDEFGH", "object": "event", "name": "Signed up to newsletter", "currency": null, "created_at": "2020-08-21 15:24:00" } ] } ``` ### Set event currency ```http POST /sites/{site_id}/events/currency ``` Set the currency of an event by its name. You can set it before the event is tracked for the first time: the currency applies as soon as the event arrives. **Permissions:** Requires write access to the site (`manage:{site_id}`). **Returns:** Returns an updated object on success. Otherwise, this call returns an error. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `name` (string, body, required): The name of the event (up to 255 characters). Example: `Purchase early access` - `currency` (string, body, required): The currency used for any value attached to this event's completions. One of: `dollar`, `pound`, `euro`, `yuan`, `peso`, `shekel`, `yen`, `won`, `hryvnia`, `franc`, `rupee`, `integer`, `none`. **Example request** ```bash curl -X POST "https://api.usefathom.com/v1/sites/CDBUGS/events/currency" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "name": "Purchase early access", "currency": "pound" }' ``` **Example response (`200`)** ```json { "object": "event", "name": "Purchase early access", "currency": "pound", "updated": true } ``` ### Delete event ```http DELETE /sites/{site_id}/events ``` Remove the currency you set for an event, so it goes back to your site's default currency. This doesn't delete the event's completion data, so the event still appears in [List events](#list-events) while it has data. The call succeeds even if the event has no currency set. **Permissions:** Requires write access to the site (`manage:{site_id}`). **Returns:** Returns a deleted object on success. Otherwise, this call returns an error. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `name` (string, query, required): The name of the event. Example: `Purchase early access` **Example request** ```bash curl -X DELETE "https://api.usefathom.com/v1/sites/CDBUGS/events?name=Purchase%20early%20access" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "object": "event", "name": "Purchase early access", "deleted": true } ``` ### Get event ```http GET /sites/{site_id}/events/{event_id} ``` > **Deprecated.** This endpoint is no longer available. Use [List events](#list-events). Previously returned a single event by its goal code. This endpoint is no longer available. **Permissions:** Requires read access to the site (`all-sites-readonly`, `read:{site_id}` or `manage:{site_id}`). **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `event_id` (string, path, required): The `id` (tracking code) of the event, as returned when the event was created. Example: `ABCDEFGH` ### Create event ```http POST /sites/{site_id}/events ``` > **Deprecated.** This endpoint is no longer available. Track the event on your site, then use [List events](#list-events) and [Set event currency](#set-event-currency). Previously created an event. This endpoint is no longer available. **Permissions:** Requires write access to the site (`manage:{site_id}`). **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `name` (string, body, required): The name of the event (up to 255 characters). Example: `Purchase early access` - `currency` (string, body, optional): The currency used for any value attached to this event's completions. If omitted, defaults to `dollar`. One of: `dollar`, `pound`, `euro`, `yuan`, `peso`, `shekel`, `yen`, `won`, `hryvnia`, `franc`, `rupee`, `integer`, `none`. Default: `dollar`. ### Update event ```http POST /sites/{site_id}/events/{event_id} ``` > **Deprecated.** This endpoint is no longer available. To change an event's currency, use [Set event currency](#set-event-currency). Previously updated an event by its goal code. This endpoint is no longer available. **Permissions:** Requires write access to the site (`manage:{site_id}`). **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `event_id` (string, path, required): The `id` (tracking code) of the event you wish to update. Example: `ABCDEFGH` - `name` (string, body, optional): The name of the event (up to 255 characters). - `currency` (string, body, optional): The currency used for any value attached to this event's completions. One of: `dollar`, `pound`, `euro`, `yuan`, `peso`, `shekel`, `yen`, `won`, `hryvnia`, `franc`, `rupee`, `integer`, `none`. ### Wipe event ```http DELETE /sites/{site_id}/events/{event_id}/data ``` > **Deprecated.** This endpoint has been retired and now returns `410 Gone`. It is no longer possible to wipe an event's completion data via the API. Previously wiped all completion data belonging to an event. This endpoint is no longer available. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `event_id` (string, path, required): The `id` (tracking code) of the event. ### Delete event ```http DELETE /sites/{site_id}/events/{event_id} ``` > **Deprecated.** This endpoint is no longer available. To delete an event, use [Delete event](#delete-event-by-name) with the event name. Previously deleted an event by its goal code. This endpoint is no longer available. **Permissions:** Requires write access to the site (`manage:{site_id}`). **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `event_id` (string, path, required): The `id` (tracking code) of the event you wish to delete. --- ## Milestones Annotate your reports with important dates. ### List milestones ```http GET /sites/{site_id}/milestones ``` Return a list of all milestones this site owns. Milestones are sorted by `created_at` ascending to allow you to paginate with ease. **Permissions:** Requires read access to the site (`all-sites-readonly`, `read:{site_id}` or `manage:{site_id}`). **Returns:** A list of milestone objects. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `limit` (integer, query, optional): A limit on the number of objects to be returned, between 1 and 100. Default: `10`. - `starting_after` (string, query, optional): A cursor for use in pagination. `starting_after` is an object ID that defines your place in the list. For example, if you make a list request and receive 10 objects ending with `obj_foo`, your subsequent call can include `starting_after=obj_foo` to fetch the next page. - `ending_before` (string, query, optional): A cursor for use in pagination, working in the opposite direction to `starting_after`. `ending_before` is an object ID that defines your place in the list. **Example request** ```bash curl "https://api.usefathom.com/v1/sites/CDBUGS/milestones" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "object": "list", "url": "/api/v1/CDBUGS/milestones", "has_more": false, "data": [ { "id": "ddc9cdff-ab83-41fa-96c6-dfb276a862e7", "object": "milestone", "name": "Website Redesign Launch", "milestone_date": "2024-01-15 00:00:00", "created_at": "2024-01-10 12:00:00", "updated_at": "2024-01-10 12:00:00" }, { "id": "7c3a1b29-5d64-4e0a-b1f2-9a8c7e6d5f43", "object": "milestone", "name": "Marketing Campaign Start", "milestone_date": "2024-02-01 00:00:00", "created_at": "2024-01-20 09:30:00", "updated_at": "2024-01-20 09:30:00" } ] } ``` ### Get milestone ```http GET /sites/{site_id}/milestones/{milestone_id} ``` Return a single milestone. **Permissions:** Requires read access to the site (`all-sites-readonly`, `read:{site_id}` or `manage:{site_id}`). **Returns:** A milestone object. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `milestone_id` (string, path, required): The `id` (UUID) of the milestone you wish to retrieve. **Example request** ```bash curl "https://api.usefathom.com/v1/sites/CDBUGS/milestones/ddc9cdff-ab83-41fa-96c6-dfb276a862e7" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "id": "ddc9cdff-ab83-41fa-96c6-dfb276a862e7", "object": "milestone", "name": "Website Redesign Launch", "milestone_date": "2024-01-15 00:00:00", "created_at": "2024-01-10 12:00:00", "updated_at": "2024-01-10 12:00:00" } ``` ### Create milestone ```http POST /sites/{site_id}/milestones ``` Create a milestone. Returns HTTP `201 Created` on success. **Permissions:** Requires write access to the site (`manage:{site_id}`). **Returns:** A milestone object. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `name` (string, body, required): The name of the milestone (up to 30 characters). - `milestone_date` (string, body, required): The date of the milestone in `YYYY-MM-DD` format. **Example request** ```bash curl -X POST "https://api.usefathom.com/v1/sites/CDBUGS/milestones" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "name": "Website Redesign Launch", "milestone_date": "2024-01-15" }' ``` **Example response (`201`)** ```json { "id": "ddc9cdff-ab83-41fa-96c6-dfb276a862e7", "object": "milestone", "name": "Website Redesign Launch", "milestone_date": "2024-01-15 00:00:00", "created_at": "2024-01-10 12:00:00", "updated_at": "2024-01-10 12:00:00" } ``` ### Update milestone ```http POST /sites/{site_id}/milestones/{milestone_id} ``` Update a milestone. Both `name` and `milestone_date` are required. **Permissions:** Requires write access to the site (`manage:{site_id}`). **Returns:** A milestone object. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `milestone_id` (string, path, required): The `id` (UUID) of the milestone you wish to update. - `name` (string, body, required): The name of the milestone (up to 30 characters). - `milestone_date` (string, body, required): The date of the milestone in `YYYY-MM-DD` format. **Example request** ```bash curl -X POST "https://api.usefathom.com/v1/sites/CDBUGS/milestones/ddc9cdff-ab83-41fa-96c6-dfb276a862e7" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "name": "Website Redesign Launch v2", "milestone_date": "2024-01-20" }' ``` **Example response (`200`)** ```json { "id": "ddc9cdff-ab83-41fa-96c6-dfb276a862e7", "object": "milestone", "name": "Website Redesign Launch v2", "milestone_date": "2024-01-20 00:00:00", "created_at": "2024-01-10 12:00:00", "updated_at": "2024-01-10 12:30:00" } ``` ### Delete milestone ```http DELETE /sites/{site_id}/milestones/{milestone_id} ``` Delete a milestone. Careful: you can't undo this, and neither can we. **Permissions:** Requires write access to the site (`manage:{site_id}`). **Returns:** Returns a deleted object on success. Otherwise, this call returns an error. **Parameters** - `site_id` (string, path, required): The ID of the site. This is the same string you use in your tracking code. Example: `CDBUGS` - `milestone_id` (string, path, required): The `id` (UUID) of the milestone you wish to delete. **Example request** ```bash curl -X DELETE "https://api.usefathom.com/v1/sites/CDBUGS/milestones/ddc9cdff-ab83-41fa-96c6-dfb276a862e7" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json { "id": "ddc9cdff-ab83-41fa-96c6-dfb276a862e7", "object": "milestone", "deleted": true } ``` --- ## Reports Custom reports across your traffic, events and sources. ### Aggregation ```http GET /aggregations ``` Build a custom report. Group and filter on the fields you care about. **Permissions:** Requires read access to the relevant site (`all-sites-readonly`, `read:{site_id}` or `manage:{site_id}`). **Returns:** Returns an array of objects. The properties of each object vary based on the aggregates and groupings you've asked for. All numeric values are returned as strings. > This API endpoint is only accurate on data from March 2021 onwards. Before then, we did not tie browser, country, pathname, etc. together, so we have no way to offer this advanced filtering on that data. > Grouped reports (`field_grouping`) default to 500 rows unless you set `limit`. The maximum is 1000, and this endpoint has no pagination. BI tools such as Looker Studio / Data Studio that fire many aggregations at once will hit the [concurrency cap](/api/v1/rate-limits), not the row cap. Space those requests, cache one extract per period, or raise the API plan. > **Goal codes are no longer available:** Reporting on an event by its goal code (`entity_id` with `entity=event`) is no longer available. Report on events using `site_id` and `entity_name` instead. Pageview reporting, where `entity_id` is the site `id`, is unaffected. **Parameters** - `entity` (string, query, required): The entity you want to report on. Events are treated separately from pageviews. Supported values: `pageview` and `event`. One of: `pageview`, `event`. - `entity_id` (string, query, Required when entity is "pageview"): When `entity` is `pageview`, this is the `id` of the site you want to aggregate on. When `entity` is `event`, do not pass a goal code here. Use `site_id` and `entity_name` instead. Pageview usage is unchanged. - `site_id` (string, query, Required when entity is "event" and entity_id is omitted): The `id` of the site the event belongs to. - `entity_name` (string, query, Required when entity is "event" and entity_id is omitted): The name of the event you want to report on. Example: `purchase`. - `aggregates` (string, query, required): The aggregates you wish to include, separated by a comma. Supported values for **pageview** entities: `visits`, `uniques`, `pageviews`, `avg_duration` and `bounce_rate`. The difference between "visits" and "uniques" is that visits are unique site visits whilst uniques are unique page visits. Supported values for **event** entities: `conversions`, `unique_conversions` and `value` (value is returned in cents). - `date_grouping` (string, query, optional): By default, we don't do any date grouping and return total aggregations. Override this with `hour`, `day`, `month` or `year`. Note: `hour` grouping is only supported for date ranges of up to 7 days. One of: `hour`, `day`, `month`, `year`. - `field_grouping` (string, query, optional): The fields you want to group by, separated by a comma (e.g. `hostname,pathname`). Supported values: `hostname`, `pathname`, `entry_page`, `exit_page`, `referrer_hostname`, `referrer_pathname`, `referrer_source`, `ai_source`, `browser`, `country_code`, `city`, `state`, `region`, `device_type`, `operating_system`, `utm_campaign`, `utm_content`, `utm_medium`, `utm_source`, `utm_term`, `keyword` and `ref`. - `sort_by` (string, query, optional): The field you want to sort by, in the format `field:asc|desc`. You can use any field present in `aggregates` or `field_grouping`. When using `date_grouping`, you can also sort by `timestamp:asc` or `timestamp:desc`. Example: `pageviews:desc` - `date_from` (string, query, optional): Timestamp (e.g. `2022-04-01 15:31:00`). Should match the timezone you're reporting in. Defaults to the entity's first recorded data. - `date_to` (string, query, optional): Timestamp (e.g. `2022-04-01 15:31:00`). Should match the timezone you're reporting in. Default: now. - `timezone` (string, query, optional): **Deprecated.** We now report using each site's configured timezone by default. If provided, this [TZ database name](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) overrides the site's timezone for this request. We'll be removing this parameter in the future. - `limit` (integer, query, optional): Optional integer from `1` to `1000` inclusive. **Default.** When `field_grouping` is set and you omit `limit`, we return at most 500 rows. When `field_grouping` is omitted, there is no default row cap. `date_grouping` alone does not trigger the 500 default. **Maximum.** A `limit` above 1000 returns HTTP 400 with an `errors.limit` validation message. There is no pagination on this endpoint, so you cannot walk past 1000 grouped rows with a cursor. Narrow the result with `filters` and/or a shorter `date_from` / `date_to`, then issue more requests. - `filters` (array, query, optional): A JSON-encoded array of filter objects. See the filtering reference below for the full list of supported properties and operators. Each filter's `value` must be a string. #### Filtering Filters are supplied as a JSON array. Each filter is an object with a `property`, an `operator` and a string `value`. You can add as many filters as you like; see the examples in the code panel. We support the following operators: - `is`: exact match - `is not`: everything except an exact match - `is like`: contains the term (supports wildcards `*`) - `is not like`: does not contain the term - `matching`: matches a regular expression (regex) pattern - `not matching`: does not match a regex pattern **Operator availability depends on the field.** Text-style fields support all six operators; categorical fields support only `is` and `is not`: - **All six operators:** `domain`, `hostname`, `pathname`, `entry_page`, `exit_page`, `referrer_hostname`, `referrer_pathname`, `referrer_source`, `ai_source`, `ref`, `utm_campaign`, `utm_source`, `utm_medium`, `utm_content`, `utm_term` - **`is` / `is not` only:** `device_type`, `operating_system`, `browser`, `country_code`, `city`, `state`, `region` Note: `domain` can be filtered on but not grouped by, while `keyword` can be grouped by but not filtered on. ##### Entry and exit pages `entry_page` is the pathname of the first pageview in a visit. `exit_page` is the pathname of the last pageview before the visitor leaves. Both are session-level fields. They mirror the Entry Pages and Exit Pages reports on your dashboard and work for both `field_grouping` and `filters`. When you filter by `entry_page`, only visits that *entered* on that page are included. A visitor who lands on `/home` and later views `/pricing` is excluded by `{"property": "entry_page", "operator": "is", "value": "/pricing"}`, but included when filtering on `pathname` instead. ##### AI tools `referrer_source` returns `AI` for visits from AI tools such as ChatGPT, Claude and Perplexity. `ai_source` returns the name of the AI tool, the same names you see in the AI tab on your dashboard (for example `ChatGPT`, `Claude`, `Perplexity` or `Gemini`). Each name combines the tool's referrers and its `utm_source` links, so `ChatGPT` includes `chatgpt.com`, `chat.openai.com` and `utm_source=chatgpt.com`. Both work for `field_grouping` and `filters`, for pageviews and events, and on all of your history. When you group by `ai_source` alone, you get one row for each AI tool and visits from anywhere else are left out. When you group it with other fields, those other visits come back with an empty `ai_source`. Read [Traffic sources](/docs/features/sources#ai-referrals) for how we decide a visit came from AI. ##### Regex examples With `matching` / `not matching` you can build sophisticated filters: - `^/(about|contact|pricing)$`: match only /about, /contact and /pricing - `^/(about|contact|pricing)`: match paths starting with those - `^/blog/\d{4}/\d{2}/`: match blog URLs like /blog/2025/07/my-post - `^/products/[^/]+/$`: match product category pages **Example request** ```bash curl "https://api.usefathom.com/v1/aggregations?entity=pageview&aggregates=pageviews" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response (`200`)** ```json [ { "browser": "Chrome", "device_type": "Desktop", "pageviews": "30000" }, { "browser": "Chrome", "device_type": "Mobile", "pageviews": "28000" }, { "browser": "Chrome", "device_type": "Tablet", "pageviews": "24000" }, { "browser": "Firefox", "device_type": "Desktop", "pageviews": "12000" }, { "browser": "Firefox", "device_type": "Tablet", "pageviews": "5000" }, { "browser": "Firefox", "device_type": "Mobile", "pageviews": "300" } ] ``` **Example response: Filter: only include the /about page (`200`)** ```json [ { "property": "pathname", "operator": "is", "value": "/about" } ] ``` **Example response: Filter: exclude /register and /login (`200`)** ```json [ { "property": "pathname", "operator": "is not", "value": "/register" }, { "property": "pathname", "operator": "is not", "value": "/login" } ] ``` **Example response: Filter: only include the United Kingdom (`200`)** ```json [ { "property": "country_code", "operator": "is", "value": "GB" } ] ``` **Example response: Filter: exclude /register or Desktop (`200`)** ```json [ { "property": "pathname", "operator": "is not", "value": "/register" }, { "property": "device_type", "operator": "is not", "value": "Desktop" } ] ``` **Example response: Filter: only include visits that entered on /pricing (`200`)** ```json [ { "property": "entry_page", "operator": "is", "value": "/pricing" } ] ``` **Example response: Filter: only include visits from ChatGPT (`200`)** ```json [ { "property": "ai_source", "operator": "is", "value": "ChatGPT" } ] ``` ### Current visitors ```http GET /current_visitors ``` Returns the total number of current visitors on a site. The detailed view also returns the top 150 pages and top 150 referrers. **Permissions:** Requires read access to the site (`all-sites-readonly`, `read:{site_id}` or `manage:{site_id}`). **Returns:** The current visitor count, with an optional detailed breakdown. **Parameters** - `site_id` (string, query, required): The `id` of the site. Example: `CDBUGS` - `detailed` (boolean, query, optional): Set to `true` for a detailed breakdown of pages and referrers. Otherwise you'll only get a count. Default: `false`. **Example request** ```bash curl "https://api.usefathom.com/v1/current_visitors?site_id=CDBUGS" \ -H "Authorization: Bearer YOUR_API_TOKEN" \ -H "Accept: application/json" ``` **Example response: Simple (`200`)** ```json { "total": 14 } ``` **Example response: Detailed (`200`)** ```json { "total": 144, "content": [ { "hostname": "bugsbunny.com", "pathname": "/spacejam", "total": 100 }, { "hostname": "bugsbunny.com", "pathname": "/blog/being-a-wabbit", "total": 44 } ], "referrers": [ { "referrer_hostname": "https://usefathom.com", "referrer_pathname": "/why-we-love-bugs-bunny", "total": 50 }, { "referrer_hostname": "https://daffyduck.com", "referrer_pathname": "/blog/i-am-sick-of-his-antics", "total": 32 } ] } ```