# Datalyr > Datalyr is a marketing attribution and analytics platform. It captures web, mobile, and > server events, resolves them to a person, joins them to ad spend and revenue, and > delivers conversions back to ad platforms. This file is the agent index from > `/llms.txt`, followed by the full text of the three pages an agent reads most. ## Agent and MCP access - [Agent access](https://docs.datalyr.com/api-reference/agents.md): What an agent can read and change, the 35 MCP tools, the write scopes, and the four rules that keep a reported number honest. - [Connect Claude or Codex](https://docs.datalyr.com/datalyr-mcp.md): Set up the hosted MCP server at `https://api.datalyr.com/mcp` over OAuth, with no stored key. ## HTTP API - [API reference](https://docs.datalyr.com/api-reference/index.md): Every endpoint at `api.datalyr.com/v1`, what each returns, which ones change something and under which scope, and the date rules. - [Authentication](https://docs.datalyr.com/api-reference/authentication.md): Create the `dk_agent_` Agent key, send it as a bearer token, and read the `401` body. - [Errors and limits](https://docs.datalyr.com/api-reference/errors-and-limits.md): Every status code with cause and fix, the 100-request rate limit and its headers, parameter caps, and pagination. - [API keys](https://docs.datalyr.com/account/api-keys.md): The two workspace keys, the named keys that carry write scopes, and how to rotate any of them. ## Sending events - [Ingest API](https://docs.datalyr.com/api-reference/ingest.md): The raw HTTP contract for `POST ingest.datalyr.com/track`: headers, body fields, batching, duplicates, and status codes. - [Web SDK](https://docs.datalyr.com/sdk-reference/web.md): Every `dl.js` method, config option, and transport limit for browser tracking. - [Node.js SDK](https://docs.datalyr.com/sdk-reference/node.md): The `@datalyr/api` server SDK, its wire format, and its retry behavior. - [iOS SDK](https://docs.datalyr.com/sdk-reference/ios.md): Swift setup, attribution, SKAdNetwork, and the full method reference. - [React Native SDK](https://docs.datalyr.com/sdk-reference/react-native.md): React Native setup, install attribution, and the full method reference. ## Getting data flowing - [Install web tracking](https://docs.datalyr.com/getting-started/install-web-tracking.md): Add the snippet to a site and start recording pageviews. - [Install mobile tracking](https://docs.datalyr.com/installation/mobile.md): Add the iOS or React Native SDK and capture installs. - [Identity](https://docs.datalyr.com/advanced/identity.md): How `visitor_id`, `user_id`, and `distinct_id` differ, and when to call `identify()`. - [Verify your setup](https://docs.datalyr.com/getting-started/verify-your-setup.md): Prove that events, revenue, and attribution all arrive. - [No events](https://docs.datalyr.com/troubleshooting/no-events.md): Symptom, cause, and fix when nothing reaches Datalyr. --- # Connect Claude or Codex Source: https://docs.datalyr.com/datalyr-mcp Connect the hosted Datalyr MCP server, and see every tool it exposes. When you finish, you can ask Claude or Codex about your Datalyr analytics without storing an API key anywhere. Server URL: `https://api.datalyr.com/mcp` The steps below ask for the scope `datalyr:read` and nothing else, so the token your client receives reads and changes nothing. It is short-lived, and it is never the workspace Agent key. Datalyr never receives your Claude or Codex credentials, and you need no Anthropic or OpenAI API key. The server does expose tools that change conversion rules and trackable links. A token that asked for `datalyr:read` alone cannot call any of them. See "The 8 tools that write or start work" below. ## Before you start You must have a Datalyr login that belongs to at least one workspace. The server checks your membership on every tool call. ## Connect Claude 1. Open Customize > Connectors in Claude. 2. Select + > Add custom connector. 3. Enter `Datalyr` as the name. 4. Enter `https://api.datalyr.com/mcp` as the remote MCP URL. 5. Select Add. 6. Select Connect. 7. Sign in to Datalyr and allow read-only access. On Claude Team or Enterprise, an Owner first adds the URL under Organization settings > Connectors. Each member then connects their own Datalyr account under Customize > Connectors. ## Connect Codex ```bash codex mcp add datalyr --url https://api.datalyr.com/mcp codex mcp login datalyr --scopes datalyr:read ``` Complete the Datalyr sign-in and the consent page in your browser. ## The 27 read tools | Tool | Returns | | --- | --- | | `list_workspaces` | Every workspace you can access. Call this first. | | `get_overview` | Visitors, pageviews, sessions, custom events, and a time series | | `get_traffic` | A breakdown by referrer, campaign, or page | | `get_devices` | A breakdown by browser, operating system, or device type | | `get_locations` | A breakdown by country, region, or city | | `get_events` | Raw events, filtered by name, campaign, country, page, or custom property | | `list_event_names` | Distinct event names of the last 30 days | | `list_event_properties` | Custom event properties this workspace sends, with a sample value and type | | `get_realtime` | Active visitors, pages, events, revenue, and a minute timeline | | `get_attribution` | Attribution by channel under a chosen model, with an unattributed bucket | | `list_users` | Visitors and identified users, with an optional search | | `get_user` | One user's profile, sessions, journey, and conversions. Covers the last 90 days by default; pass `window_days` (up to 365) for longer history | | `get_ads` | Provider-reported spend, delivery, conversions, and ROAS | | `get_revenue` | Revenue, orders, subscriptions, refunds, AOV, and a time series | | `get_commerce_metrics` | Certified revenue, spend, profit, margin, ROAS, and MER | | `list_metrics` | The catalog of metric ids you can query, with units and availability | | `query_metrics` | Up to 50 named metrics over a date range, with an optional comparison | | `get_retention` | Person-grain retention cohorts, with active persons and revenue per period | | `get_ltv` | Cohort LTV: cumulative revenue and revenue per acquired person, period by period | | `get_funnel` | An ordered funnel over 2 to 5 event names, with per-step conversion rates | | `get_tags` | Tracking-link tag counts and percentages | | `get_workspace` | Name, domain, timezone, and connected platforms | | `get_onboarding_status` | Plan, whether tracking has ever sent an event, live sources, connection health, and the next steps a person must take | | `get_usage` | Plan, and current-period event and postback usage | | `search_docs` | Matching pages of this documentation, with a link and an excerpt | | `get_script_proposals` | Container-script drafts an agent submitted, and what a human decided | | `get_export_status` | One export job's state, and its download URL once the file exists | ## The 8 tools that write or start work The server advertises 8 more tools. A token that asked for `datalyr:read` alone cannot call any of them. See [Agent access](/api-reference/agents) for the scopes, the dry-run default, and the undo path. | Tool | Does | Scope | | --- | --- | --- | | `create_conversion_rule` | Creates a conversion rule. Dry run by default. | `datalyr:write:rules` | | `update_conversion_rule` | Edits a conversion rule. Dry run by default. | `datalyr:write:rules` | | `delete_conversion_rule` | Deletes a conversion rule. Dry run by default. | `datalyr:write:rules` | | `create_link` | Creates a trackable link. Dry run by default. | `datalyr:write:links` | | `update_link` | Edits a trackable link. Dry run by default. | `datalyr:write:links` | | `delete_link` | Deletes a trackable link. Dry run by default. | `datalyr:write:links` | | `propose_container_script` | Submits a container-script draft for review. It publishes nothing. | `datalyr:propose:scripts` | | `create_export` | Starts an export job. It returns a job id, never data. | `datalyr:read` | No tool changes tracking, settings, sources, tracking domains, team membership, or billing. No scope publishes a container script. There is no signup tool. MCP needs an OAuth token, and only an existing Datalyr account can hold one. To start a signup for somebody who has no account, call `POST /v1/signup`. See [Agent-driven setup](/developer/agent-driven-setup). ## Rules the client follows | Rule | Detail | | --- | --- | | Workspace selection | With one workspace, `workspace_id` can be omitted. With several, pass the ID from `list_workspaces`. | | Date defaults | Omit both dates and the tool uses the last 30 days. | | Row caps | `get_events` and `list_users` return 1000 rows maximum. `get_ads` returns 200. `get_traffic` returns 100. | | Ad platforms | `get_ads` accepts `meta`, `google`, or `tiktok`. It keeps each platform's money separate. | Tool results carry customer, campaign, and revenue data. Connect only clients and organizations you trust, because anything you connect can read all of it. An AI client can misread a number it fetched correctly. Check any high-impact decision against the matching report in Datalyr. ## Verify 1. Start a new conversation. 2. Ask: `List my Datalyr workspaces.` 3. Confirm the reply names the workspace you expect. 4. Ask: `Show my Datalyr overview for the last 7 days.` 5. Compare the visitor count with Overview in Datalyr. ## When it does not work | Symptom | Cause | Fix | | --- | --- | --- | | The client reports a `401` | The OAuth token expired or was revoked. | Remove the connector and add it again. | | `list_workspaces` returns nothing | Your login belongs to no workspace. | Ask an owner or admin to invite you. | | A tool reports an unknown workspace | The client passed a stale ID. | Ask it to call `list_workspaces` again. | | A revenue answer looks wrong | `get_revenue` and `get_commerce_metrics` answer different questions. | Use `get_commerce_metrics` for profitability. | | The client reports empty data | The date range holds no events. | Ask for a wider range. Confirm events in Events. | Removing Datalyr from your client's connector settings stops it from requesting new data. --- # Authentication Source: https://docs.datalyr.com/api-reference/authentication Create an Agent key and authorize every Datalyr API request with it. When you finish, a `curl` request to `https://api.datalyr.com/v1/workspace` returns your workspace instead of `401`. ## Before you start You must have the `owner` or `admin` role in the workspace. A `member` cannot generate or regenerate a key. ## The two workspace keys Settings > API holds exactly one key of each type. The two are not interchangeable. | Key | Prefix | Length after the prefix | Use it for | Accepted by `/v1` | | --- | --- | --- | --- | --- | | Agent key | `dk_agent_` | 32 characters | Every server-side or agent read of Datalyr data | Yes | | Write key | `dk_` | 32 characters | SDK event ingest, and the attribution lookup the mobile SDKs run for themselves | No | The `/v1` handler reads the `Authorization` header, requires the `Bearer` scheme, and requires the token to start with `dk_agent_`. A write key fails that prefix test, so it returns `401`. The write key is a send-events credential. Reading Datalyr data from your own server, or from an agent, always uses the Agent key. The Agent key reads. It never changes anything, because it resolves to `datalyr:read` alone and always will. To change a conversion rule or a trackable link over `/v1`, an owner or admin mints a named key and grants it that scope explicitly. See the Agent access page. `POST /attribution/lookup` is the one exception, and it is not a general read route. The iOS and React Native SDKs call it to resolve the install they already own, using the email that same app collected. Do not call it from your own backend with an email a user typed. Pointed at arbitrary addresses, it turns a write key into a way to read another person's attribution history. Keep the Agent key on your server. It reads every event, user, and revenue row in the workspace. Never put it in a browser bundle, a mobile app, or a public repository. ## Create the key 1. Open Settings > API. 2. Find the Agent key card. 3. Select generate key, or regenerate when a key already exists. 4. Confirm in the dialog. 5. Copy the full value from the one-time dialog. Copy the key before you close the dialog. We store a SHA-256 hash for lookup and never show the full value again, so a lost key means regenerating, which breaks every client still holding the old one. Regenerating invalidates the previous key at once, with no overlap window. We drop the cached lookup for the old hash in the same action. ## Send the key ```bash curl "https://api.datalyr.com/v1/overview?start_date=2026-07-01T00:00:00Z&end_date=2026-07-25T00:00:00Z" \ --header "Authorization: Bearer dk_agent_YOUR_KEY" ``` | Header | Value | | --- | --- | | `Authorization` | `Bearer dk_agent_…` | The key identifies the workspace on its own. No workspace ID or header goes in the request. ## What a rejected request looks like A rejected request returns HTTP `401`, the header `WWW-Authenticate: Bearer realm="Datalyr Read API"`, and this body: ```json { "error": "Invalid or missing Agent API key. Use: Authorization: Bearer dk_agent_xxxxx" } ``` Five conditions produce it: no `Authorization` header, a header that is not two space-separated parts, a scheme other than `Bearer`, a token without the `dk_agent_` prefix, and a token whose hash matches no workspace. We cache both a valid and an invalid key lookup for 300 seconds, so a fresh key can take that long to take effect everywhere. ## Verify Run the `curl` command above against `/v1/workspace`. A `200` response with your workspace `name` proves the key works. ## When it doesn't work | Symptom | Cause | Fix | | --- | --- | --- | | `401` right after you paste the key | You copied the masked preview, not the full value. | Regenerate the key. Copy the value from the one-time dialog. | | `401` on a key that worked yesterday | Someone regenerated the key. | Open Settings > API. Regenerate, and update every client. | | `401` on a `dk_` key | You sent the write key. | Send the Agent key. The write key never reads `/v1`. | | No generate key button | Your role is `member`. | Ask an `owner` or `admin` to generate the key. | --- # Errors and limits Source: https://docs.datalyr.com/api-reference/errors-and-limits Every Datalyr API status code with its cause and fix, plus the exact rate and size limits. ## Error shape Every error is JSON with an `error` string: ```json { "error": "start_date and end_date required (ISO 8601)" } ``` `/ads` adds a stable `code`. `/commerce-metrics` and the cost-import routes add a stable `reason`. Branch on `code` or `reason` rather than the `error` text, which we reword. ```json { "error": "Ads analytics query failed or timed out", "code": "ads_provider_read_failed" } ``` ## Status codes | Status | `error` | Cause | Fix | | --- | --- | --- | --- | | `400` | `start_date and end_date required (ISO 8601)` | One date is missing, or `Date` cannot parse it. | Send both as ISO 8601, such as `2026-07-01T00:00:00Z`. | | `400` | `type must be browser, os, or deviceType` | `/devices` got another `type`. | Use `browser`, `os`, or `deviceType`. | | `400` | `offset + limit cannot exceed 5000. Narrow the date range or use search` | `/users` asked for a page past the 5000-row window. | Narrow the date range, or use `search` instead of paging deeper. | | `400` | `use date_from/date_through or start_date/end_date, not both` | `/ads` got both date forms. | Send one form. | | `400` | `Ads date ranges must contain 1-366 days` | The `/ads` range is empty or longer than 366 days. | Split the range into 366-day pages. | | `400` | `platform must be meta, google, or tiktok` | `/ads` got another `platform`. | Use `meta`, `google`, or `tiktok`. | | `400` | `cursor is invalid or belongs to another sort` | The `/ads` `cursor` came from a request with a different `sort`. | Restart the page walk. Keep `sort` identical across pages. | | `400` | `start_date and end_date must be UTC hour instants with start < end` | `/commerce-metrics` got instants that are not on the hour, or out of order. | Round both to an exact UTC hour. Put `start_date` first. | | `400` | `requested interval cannot exceed 366 days` | The `/commerce-metrics` interval is too long. | Request 366 days or fewer. | | `400` | `end_date cannot be in the future` | `/commerce-metrics` got a future `end_date`. | Use the current hour or earlier. | | `400` | `Query parameter … is not allowed` | A request tried to override the commerce policy through the query string. | Remove the parameter. The policy is server-held. | | `401` | `Invalid or missing Agent API key. Use: Authorization: Bearer dk_agent_xxxxx` | No key, wrong scheme, a `dk_` write key, or an unknown key. | Send the Agent key. | | `403` | Payload with `code: insufficient_scope` | The endpoint changes something, and your key does not carry the scope for it. | Read `required_scope` on the response. Ask an owner for a named key that holds it. | | `403` | `A workspace agent API key plus operator authorization is required for cost imports` | You called a cost-import route. | Do not call it. Datalyr operates these two routes. | | `404` | `Not found` | The path is not one of the `/v1` endpoints. | Compare the path with the endpoint tables. | | `404` | `User not found` | `/users/{userId}` found no event for that ID inside `window_days`, which defaults to 90 days. | Take an ID from `/users`. If that user's last event is older than the window, send `window_days=365`. | | `404` | `Ad account not found in this workspace` | The `/ads` `account_id` belongs to another workspace. | Drop `account_id`, or use an account the workspace owns. | | `405` | `Method not allowed` | The path exists, but not for the method you sent. | Use the method the endpoint tables name for that path. | | `429` | `Rate limit exceeded` | The key passed 100 requests in 60 seconds. A write also returns this when the rate-limit store is unreachable. | Wait for `Retry-After`, which is `60`. Then retry. | | `500` | `Internal server error` | A handler threw, or a Tinybird query passed its 10-second timeout. | Retry once. If it repeats, narrow the date range and contact support. | | `503` | `Ads analytics query failed or timed out` | Code `ads_provider_read_failed`. The ad-metrics store did not answer. | Retry with backoff. Ad data is unchanged. | | `503` | `Ads analytics storage is not configured` | Code `ads_storage_unavailable`. | Contact support. No client-side fix exists. | | `503` | Commerce payload with a `reason` field | `/commerce-metrics` could not certify the answer. | Read `reason`. `commerce_policy_does_not_cover_interval` means the policy starts after your `start_date`. | Never treat `503` from `/ads` or `/commerce-metrics` as zero spend or zero revenue. These endpoints fail loudly on purpose, so that a read you could not complete never lands in a report as a real number. ## Rate limit | Limit | Value | | --- | --- | | Scope | One key, across every `/v1` endpoint. The per-workspace Agent key is one key. | | Window | 60 seconds, sliding | | Requests per window | 100 | | Response over the limit | `429` with `Retry-After: 60` | Write endpoints spend a second, smaller budget on top of this one: 20 write requests per key per minute, including dry runs. A write therefore needs room in both budgets. The limit counts requests, not rows. One `/events` request for 1000 rows costs the same as one `/workspace` request. Every `/v1` response carries your current budget, so you do not have to be rejected to find it. | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Requests allowed per window. Normally `100`. | | `X-RateLimit-Remaining` | Requests left in the current 60-second window. `0` on a `429`. | | `X-RateLimit-Degraded` | Present and `1` only while the limit store is unreachable. Absent otherwise. | The first two headers are present on success and on error responses. ### When the limit store is unreachable Reads and writes behave differently on purpose. | Path | Behavior | What you see | | --- | --- | --- | | Reads | Fail open, under a smaller standby limit | Requests still succeed. `X-RateLimit-Degraded: 1`, and `X-RateLimit-Limit` reads `30`. | | Writes | Fail closed | `429` with `Retry-After: 60`, as if you were over the limit. | A read outage is a wasted query, so a store outage must not take every read down with it. Reads fall back to a standby limit of 30 requests per 60 seconds. That limit is per server, not per key, so your effective budget during an outage is smaller than 100 and is not exactly predictable. Treat `X-RateLimit-Degraded: 1` as a signal to slow down. A write that lands twice costs a duplicate conversion rule or a duplicate postback, so writes stop rather than guess. Send an `Idempotency-Key` on every write, and a retry after a store outage stays safe. ## Parameter limits | Endpoint | Parameter | Minimum | Maximum | Default | | --- | --- | --- | --- | --- | | `/events` | `limit` | 1 | 1000 | 100 | | `/events` | `offset` | 0 | No maximum | 0 | | `/events` | `event_name` | No minimum | 50 comma-separated names, 200 characters each | No default | | `/users` | `limit` | 1 | 1000 | 100 | | `/users` | `offset` | 0 | `offset` + `limit` must be 5000 or less | 0 | | `/users/{userId}` | `window_days` | 1 | 365 | 90 | | `/traffic` | `limit` | 1 | 100 | 20 | | `/tags` | `limit` | 1 | 100 | 20 | | `/devices`, `/locations` | `limit` | 1 | 100 | 15 | | `/realtime` | `time_window` minutes | 1 | 60 | 30 | | `/realtime` | `visitors` returned | No minimum | 50 | No default | | `/attribution` | `window_days` | 1 | 365 | 30 | | `/attribution` | campaign rows returned | No minimum | 100 | No default | | `/ads` | `limit` | 1 | 200 | 50 | | `/ads` | `cursor` characters | No minimum | 500 | No default | | `/ads` | `q` characters | No minimum | 200 | No default | | `/ads`, `/commerce-metrics` | Range days | 1 | 366 | No default | We clamp a value above the maximum instead of returning `400`, so an oversized `limit` still gives you a page of data. ## Pagination `/events` and `/users` page with `limit` and `offset`, and both return `has_more`. Walk a page at a time, adding `limit` to `offset`, and stop when `has_more` is `false`. Do not stop on a short page instead, because a full range can still end on one. Keep every other parameter identical across the walk, and keep the date range fixed, or the pages will overlap. On `/users`, `offset` plus `limit` must stay at 5000 or less, and a larger window returns `400` rather than a truncated page. Narrow the date range or pass `search` when you need to go deeper. On `/events`, `total_count` counts the whole range, not the page. With `property_filters` and no `include_total`, it is exact on the last page and a lower bound before it, flagged by `total_count_is_lower_bound: true`. Send `include_total=true` for the exact count on every page. That count reads the range a second time, so the request takes longer. ## Ingest limits Event ingest runs on a different host, `https://ingest.datalyr.com`, and has its own limits. The two ingest transports carry different limits. `/track` takes one server event per request. The SDK batch transport takes an array. | Limit | Applies to | Value | Response over the limit | | --- | --- | --- | --- | | Requests per write key per second | `/track` | 1000 | `429` with `Retry-After: 1` | | Request body | `/track` | 256,000 bytes | `413` `Payload too large. Maximum size is 256KB.` | | Events per workspace per minute | SDK batch transport | 20,000 | `429` with `Retry-After: 60` | | Request body | SDK batch transport | 1 MB | `413` `Payload too large` | | `Content-Type` | SDK batch transport | Must match `application/json` | `415` | | SKAdNetwork report body | `/.well-known/skadnetwork/report` | 16 KB | `413` | The batch limit counts events, not requests. A batch of 50 events costs 50. The `/track` limit counts requests, which is the same thing there, because each request carries one event. ## Verify a limit Send 101 `/v1/workspace` requests inside one minute. Request 101 returns `429` and the header `Retry-After: 60`.