Add my site

API reference Advanced

Read analytics and manage the resources listed below over JSON. Each operation names the permission it needs (listing your sites needs none). The MCP server, CLI and /api/openapi.json share the route catalogue. The Shopify backfill endpoint documented separately below is not yet in that catalogue.

Authentication

Create a key on your account page or on a site's Settings → API tab; it starts with dsk_ and is shown once. Send it as a Bearer header on every request:

curl -H "Authorization: Bearer dsk_YOUR_KEY" https://app.datastated.com/api/sites

A key cannot create a child key with broader permissions or site access than its own. A key sees the sites its owner belongs to. It can also be limited at creation to a list of sites; a key with that list can never address another site, even one its owner belongs to, and such a site answers as if it did not exist.

Permissions

Every key carries a list of permissions and every route names the one it needs. A route refuses with 403 and names the missing permission in missingScope. The grammar is family:action: a route asks for funnels:write, and a key may hold that, the family wildcard funnels:*, or * for everything. Keys made before permissions existed hold *. The key form offers these:

PermissionLabelWhat it grants
analytics:readRead analyticsTraffic, revenue, breakdowns, visitors, claims and the live count.
events:writeRecord eventsSend orders and goals from your own server.
identify:writeIdentify visitorsAttach a user id or hashed email to a visitor.
sites:writeCreate sitesAdd a new site to your account. Deleting a site stays in the dashboard.
team:*Manage peopleSee who has access, invite someone, remove someone.
keys:*Manage API keysList, create and revoke keys on your account.
funnels:*Manage funnelsSee, create, change and delete funnels, and read their results.
alerts:*Manage alertsSee, create, switch and delete email alert rules.
annotations:*Manage chart notesSee, add and delete the notes pinned to chart days.
segments:*Manage segmentsSee, save and delete saved filters.
settings:*Manage settingsExclusions, the primary goal, the weekly summary, the public page, integrations and sync.
*EverythingEvery permission above, now and as new ones are added.

Rate limit

Analytics date ranges use real yyyy-mm-dd dates, inclusive, with from on or before to, and span at most 1,830 days. Timeseries requests are limited to 2,000 points. Shorten the range or request a coarser grain when a request is rejected.

Authenticated API keys and sessions share a PostgreSQL-backed limit across replicas using the same database; restarting a web process does not reset that shared window. Anonymous requests and unrecognized credentials still pass through process-local limits. If authentication or the shared limiter cannot be checked, the request returns 503 with Retry-After, rather than falling back to a local-only allowance. Self-hosted instances must run the database migration that installs the limiter.

600 requests a minute per credential, in a sliding window. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (unix seconds when a slot next frees up). Past the limit the answer is 429 with a Retry-After header and the body {"status": "error", "code": "rate_limited", "error": "Too many requests. This key may make 600 requests a minute; try again in 12s.", "retryAfter": 12}. An MCP tool call counts once, not twice.

Two response shapes

Read each endpoint's scope before comparing it with a dashboard panel. The claims comparison is per platform; campaign rows carry platform-reported figures. In claims, verifiedOrders and verifiedRevenue require Stripe charge records or reconciled Shopify payments; unverifiedOrders and unverifiedRevenue hold self-reported orders and historical Shopify orders without payment evidence. Other analytics views count recorded orders, not just processor-confirmed ones.

Routes added with the catalogue answer an envelope: {"status": "ok", "data": ..., "meta"?, "pagination"?} on success (201 when a POST creates something; the action POSTs, such as enabling the public page or syncing an integration, answer 200), and {"status": "error", "code", "error", ...} on failure, where error is a sentence and code is stable: bad_request 400, unauthorized 401, payment_required 402 (the site is paused behind plan selection), forbidden 403, not_found 404 (no such site, or not yours, or your role is too low), conflict 409, rate_limited 429, internal 500. The routes that predate it answer their bare payload and {"error": "..."} bodies, unchanged so nothing built on them breaks. They are marked legacy in the table: GET /api/sites, /window, /claims, /live, /crawls (both verbs), /search, /metrics, /events, /identify, /sync and /billing. The 403 for a missing permission uses the envelope on old and new routes alike, and still carries error.

Lists page with ?limit= (1 to 500, default 50) and ?cursor=, the pagination.nextCursor of the previous page, null on the last. Routes marked filters take the dashboard's own grammar: f_channel=google-ads, f_country=US&f_country=DE, f_device.not=mobile, f_page.contains=pricing, f_page.matches=/docs/*, f_goal=signup&f_goal.prop.plan=pro. Dimensions: channel, utm_campaign, utm_term, utm_content, referrer, page, entry, goal, action, country, region, city, device, browser, os, visit. Operators: is (no suffix), not, contains, not_contains, starts_with, ends_with, matches. A filter picks visitors: everyone with at least one matching event, and all of their events count.

Money and imported history

Numeric analytics money is USD only, with no exchange-rate conversion. Order counts include other currencies. The window carries totals.money with reportingCurrency: "USD", conversion: "none", excludedEventCount and per-currency currencies totals. Missing currency is reported as UNKNOWN, not assumed to be USD. Read these fields before interpreting a zero.

The older /metrics summary is all-or-nothing: it returns 422 with code: "currency_unavailable" and currencyScope if any amount in the requested range is non-USD or has no known currency. It does not add those amounts into a dollar total. Its legacy /api/clients/{slug}/metrics alias follows the same rule. A site sync can succeed at importing data while returning reportStatus: "currency_unavailable"; raw data stays available even when a dollar report cannot be generated.

In claims, spend, claimed revenue, CPC and platform-reported ROAS are available for known USD ad records. Each platform and campaign carries money.comparable and per-currency totals; unknown, mixed or non-USD amounts leave combined dollar fields null, without conversion. Revenue gaps also require compatible USD processor-confirmed payments. Read the response-level money.comparisonsAvailable, each row's currency scope and nullable monetary fields. Older ad rows with unknown currency need a re-sync. Campaign rows remain platform reports, not matched payment records.

Unfiltered window totals, charts and weekly summaries share imported daily visitor/pageview history. When imports contribute, totals.trackedVisitors, totals.importedVisitors and totals.importedPageviews separate the counts. windowMeta.importedDayCount and windowMeta.visitorCountMode describe the scope. Imported visitors are daily counts, not deduplicated people across the range; imports add no revenue or journey identities. See import rules.

Catalogue endpoints

Generated from the route catalogue. Role is the site role the key's owner needs. Destructive rows are the ones the MCP server dry-runs unless told confirm: true, and the CLI asks about. Ranged reads take ?from= and ?to= as yyyy-mm-dd, UTC, defaulting to the trailing 30 days.

ResourceRouteWhat it doesPermissionNotes
SitesGET /api/sitesThe sites this key may see.any keylegacy
POST /api/sitesAdd a site to your account; you become its owner.sites:write
AnalyticsGET /api/sites/{site}/windowThe full analytics window for one site, defaulting to the trailing 30 days.analytics:readviewer, legacy
GET /api/sites/{site}/claimsPlatform claims beside orders attributed to its paid channel. Campaign rows contain platform claims only.analytics:readviewer, legacy
GET /api/sites/{site}/liveHow many visitors the site had in the last five minutes.analytics:readviewer, legacy
GET /api/sites/{site}/crawlsWhat search engines and AI assistants fetched from the site, from its own server's reports.analytics:readviewer, legacy
GET /api/sites/{site}/searchGoogle Search Console: top terms and landing pages, with the site's own orders beside each page.analytics:readviewer, legacy
GET /api/sites/{site}/metricsThe money-side PeriodSummary (spend, claimed and ledger revenue, MER) over a range.analytics:readviewer, legacy
Server eventsPOST /api/sites/{site}/eventsRecord a goal, order or refund from your own server; immune to ad blockers.events:writeowner, legacy
Crawler reportsPOST /api/sites/{site}/crawlsReport raw fetches from your own server so crawler analytics can name who read the site.events:writeowner, legacy
IdentifyPOST /api/sites/{site}/identifyAttach a user id, a hashed email and up to 10 properties to a visitor.identify:writeowner, legacy
SyncPOST /api/sites/{site}/syncRun the whole sync pipeline for a site: ad connections, Stripe, rollups, report.settings:writeowner, legacy
Fee settingsGET /api/sites/{site}/billingThe site's fee configuration (fee percent and basis), or null.settings:readowner, legacy
PUT /api/sites/{site}/billingSet or clear the site's fee configuration.settings:writeowner, legacy
BreakdownsGET /api/sites/{site}/breakdowns/{kind}One dashboard panel as rows: sources, pages, countries, devices, goals and the rest.analytics:readviewer, filters, paged
TimeseriesGET /api/sites/{site}/timeseriesThe range as hourly, daily or weekly buckets, with the previous period beside it.analytics:readviewer, filters
VisitorsGET /api/sites/{site}/visitorsThe range's visitors, latest activity first.analytics:readviewer, filters, paged
GET /api/sites/{site}/visitors/{id}One visitor's whole journey, oldest event first.analytics:readviewer
SegmentsGET /api/sites/{site}/segmentsList the site's saved segments (named dashboard filters).segments:readviewer
POST /api/sites/{site}/segmentsSave a segment. Saving a name that exists replaces its filter.segments:writeowner
GET /api/sites/{site}/segments/{id}One saved segment.segments:readviewer
PUT /api/sites/{site}/segments/{id}Rename a segment and/or change its filter.segments:writeowner
DELETE /api/sites/{site}/segments/{id}Delete a saved segment.segments:writeowner, destructive
FunnelsGET /api/sites/{site}/funnelsList the site's funnels (their definitions, not their counts).funnels:readviewer
POST /api/sites/{site}/funnelsCreate a funnel from a name and 2 to 8 ordered steps.funnels:writeowner
GET /api/sites/{site}/funnels/{id}One funnel's definition.funnels:readviewer
PUT /api/sites/{site}/funnels/{id}Rename a funnel and/or replace its steps.funnels:writeowner
DELETE /api/sites/{site}/funnels/{id}Delete a funnel.funnels:writeowner, destructive
GET /api/sites/{site}/funnels/{id}/resultsCount a funnel over a range, optionally under a dashboard filter.funnels:readviewer, filters
AlertsGET /api/sites/{site}/alertsList the site's alert rules.alerts:readviewer
POST /api/sites/{site}/alertsAdd an alert rule. Its cursor starts now, so history is never emailed.alerts:writeowner
GET /api/sites/{site}/alerts/{id}One alert rule.alerts:readviewer
PUT /api/sites/{site}/alerts/{id}Switch a rule on or off and/or change who it emails.alerts:writeowner
DELETE /api/sites/{site}/alerts/{id}Delete an alert rule and its send ledger.alerts:writeowner, destructive
Chart notesGET /api/sites/{site}/annotationsChart markers (owner notes and synced commits) dated inside a range, oldest first.annotations:readviewer
POST /api/sites/{site}/annotationsAdd a note to one UTC day of the chart.annotations:writeowner
DELETE /api/sites/{site}/annotations/{id}Delete a note you wrote. Teammates' notes and commit markers answer 404.annotations:writeowner, destructive
Weekly summaryGET /api/sites/{site}/digestThe weekly summary email settings: switch, timezone, recipients, last send.settings:readowner
PUT /api/sites/{site}/digestChange the weekly summary: switch it on or off, set the timezone, set the recipients.settings:writeowner
Public pageGET /api/sites/{site}/public-dashboardThe public read-only dashboard: whether it is on, its link, and its two options.settings:readowner
PUT /api/sites/{site}/public-dashboardSet the public page's options: show revenue, allow search engines.settings:writeowner
POST /api/sites/{site}/public-dashboard/enableSwitch the public page on. The first time mints its link.settings:writeowner
POST /api/sites/{site}/public-dashboard/disableSwitch the public page off. The link is kept, so enabling again restores it.settings:writeowner
POST /api/sites/{site}/public-dashboard/regenerateMint a new public link. The old link stops working immediately.settings:writeowner, destructive
SettingsGET /api/sites/{site}/settingsThe site's settings: exclusions, primary goal, renewal revenue, proxy subdomain, verification.settings:readowner
PUT /api/sites/{site}/settingsChange the site's settings. Only the fields sent change.settings:writeowner
IntegrationsGET /api/sites/{site}/integrationsWhether each integration is connected, and when it last synced.settings:readviewer
POST /api/sites/{site}/integrations/{kind}/disconnectDisconnect an integration, exactly as the settings card's button does.settings:writeowner, destructive
POST /api/sites/{site}/integrations/{kind}/syncPull from an integration now, the same pass the worker runs on its schedule.settings:writeowner
PeopleGET /api/sites/{site}/teamWho has access to the site, and who has been invited but not yet accepted.team:readowner
POST /api/sites/{site}/team/invitesInvite someone by email as an owner or a viewer.team:writeowner
DELETE /api/sites/{site}/team/invites/{inviteId}Withdraw a pending invite before it is accepted.team:writeowner, destructive
PUT /api/sites/{site}/team/{userId}Change what a member can do. Refuses to demote the last owner.team:writeowner
DELETE /api/sites/{site}/team/{userId}Take a member's access away. Refuses to remove the last owner.team:writeowner, destructive
API keysGET /api/keysThe API keys on your account. Never their values.keys:read
POST /api/keysCreate a key within the caller’s permissions and site restrictions.keys:write
DELETE /api/keys/{id}Revoke one of your keys. It stops working on its next request.keys:writedestructive

Shopify backfill

POST /api/shopify/{site}/backfill uses an owner key with settings:write. It reconciles historical orders against Shopify payment transactions. This separate endpoint returns an envelope with its cursor in data.after, not pagination.nextCursor. An empty JSON body requests the last 30 days; explicit UTC from and to allow a window of at most 60 days. Follow the request, permission and paging instructions. The generic integration sync is not this backfill, and there is no dedicated CLI or MCP command yet.

The older write bodies

POST /events records a goal, an order or a refund from your own server, idempotent by event_id; a visitor_id (from window.datastated.visitorId) makes it inherit that visitor's attribution. It answers 201 {"stored": true, "deduplicated": false}, or 200 with deduplicated: true on a repeat. A body carrying stripe_checkout_session_id and visitor_id instead joins that Checkout Session to the visitor and reads the settled amount from Stripe, never from the caller; it answers {"stored", "match", "order"}, described on the Stripe page.

Its fields: kind (goal, order or refund), name and up to 10 props for a goal, value and currency, visitor_id, event_id, order_id (the order an order or refund belongs to), email (only its SHA-256 is stored) and timestamp, ISO 8601, defaulting to now.

POST /identify attaches a user_id, a hashed email and up to 10 props to a visitor_id, with the rules on the goals page. POST /sync runs the site's whole sync pass now and lists what each source upserted.

OpenAPI and the playground

GET /api/openapi.json is an OpenAPI 3.1 document generated from the same catalogue on each request, public because it describes routes and never data. Each operation carries x-scope and x-role, plus x-destructive or x-legacy where they apply; tags are the resources above. Point a client generator at it, or open the API playground in the app at /api-playground: every endpoint, with a way to run the reads as yourself on your signed-in session. Writes are not run from there; they show the curl and CLI lines to run with a key.

Questions? Email us at hello@datastated.com.