Reference
API reference
The API Worker serves every endpoint on this page from the same origin as the dashboard.
The examples use https://analytics.example.com. The MCP server has its own page,
Ask AI tools about your traffic.
Authentication
Send a bearer token in the Authorization header:
curl -H "Authorization: Bearer mm_..." "https://analytics.example.com/api/v1/stats/aggregate?site=shop&period=7d"| Prefix | What it is | How you get it |
|---|---|---|
mm_ | API key | Settings > Integrations in the dashboard, for one site, or POST /api/v1/keys, for one site or all of them. A key acts with its owner's role. |
ms_ | Browser session | POST /api/v1/auth/login. The dashboard stores it. It lasts 30 days. |
sh_ | Shared link | POST /api/v1/share/:slug. Read-only, one site, with the link's segment applied to every query. |
The API reads tokens from the header only. A token in the query string is ignored, so a key never lands in logs or browser history. Requests without a token can read public sites, such as the demo.
Who may call a route
The Role column in the tables below names the least access a route needs.
| Role | Who has it |
|---|---|
| Public | Anyone. No token is needed. |
| Any | Any token, or none. The answer depends on who asks. |
| Viewer | The read permission on the site: a team owner, admin, editor, or viewer, or a guest editor or viewer of that site. A route that needs Viewer also answers for a public site without a token, and for a shared link's site with its sh_ token, unless the row says otherwise. |
| Editor | The edit permission: a team owner, admin, or editor, or a guest editor of the site. |
| Admin | The manage permission: a team owner or admin. |
| Owner | The own permission: a team owner. |
| Session | A signed-in user with an ms_ token. An API key gets 403 with API keys cannot do this; sign in to the dashboard, and a request without a token gets 401. |
A team role covers every site of the team. A guest role covers one site. The billing role has no site permissions, so a billing member reads no stats. An API key has its owner's role, and a key limited to one site has no role on any other site.
Errors
Every error is JSON with one field:
{ "error": "unknown property: pagee" }| Status | Meaning |
|---|---|
400 | The request is invalid: an unknown period, dimension, or filter, a custom range over 400 days, a missing or malformed field, or a body that is not valid JSON. |
401 | No token, a token that expired, an unknown API key, or a wrong password. |
403 | Your role on the site or team does not allow this action, or the route needs a browser session and got an API key. |
404 | The site or the resource does not exist. Unknown /api/* paths also return 404. |
405 | GET /mcp. The MCP server takes POST only. |
409 | A conflict: a domain or site id that is already registered, a goal name that already exists, an email that already has an account or is already on the team, a team that still has sites, a change that would leave a team without an owner, an import while the site's history moves between shards, or search terms for a site without Search Console. |
413 | The body is over the limit: 256 KB for a payment webhook, 64 KB for /mcp. POST /api/event drops a body over 16 KB and still answers 202. |
429 | Over a rate limit. Try again after a minute. |
500 | An unexpected error. The body is {"error": "internal error"}. |
501 | The server lacks what the feature needs: the managed proxy without CF_API_TOKEN, CF_ZONE_ID, and PROXY_CNAME_TARGET, Google import and Search Console without GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET, or a PDF report preview without the BROWSER binding. |
502 | Google or the Cloudflare API returned an error. The message carries theirs. |
503 | Email could not go out, because no email provider is set up or the provider refused the message. A PDF preview that Browser Run could not print also returns 503. |
/mcp answers JSON-RPC requests with JSON-RPC errors (Ask AI tools about your traffic). A payment
webhook answers {"error": ...} with its own statuses, listed in
Payment webhooks.
Rate limits
/api/v1/stats/* and /api/v2/query allow 100 requests per minute per API key, or per
IP address for requests without a key (STATS_LIMIT). Signed-in dashboards and shared
links are not limited.
POST /api/v1/share/:slug, POST /api/v1/auth/signup, POST /api/v1/auth/login,
POST /api/v1/auth/forgot, POST /api/v1/auth/reset, and
POST /api/v1/invitations/:token/accept use the same limit per IP address, counted
separately for each route. Over it, they return 429 with too many attempts, wait a minute.
Query parameters
All GET /api/v1/stats/* endpoints accept these parameters.
| Parameter | Values | Default |
|---|---|---|
site | A site id. site_id is accepted as an alias. | Required |
period | realtime, today, yesterday, 24h, 7d, 28d, 30d, 90d, mtd, lastmonth, ytd, 6mo, 12mo, all, custom | 30d |
from, to | YYYY-MM-DD in the site's time zone. Required with period=custom. Both days are included. At most 400 days. | |
filters | See Filters | None |
compare | previous (the period of the same length before), year (the same dates a year earlier), true or 1 (the same as previous), or 0. Any other value compares nothing. | 0 |
interval | minute, hour, day, week, month | Picked from the period |
imported | 0 leaves imported history out | Imported history is included in unfiltered reports |
Days, weeks, and months follow the site's time zone, including daylight saving changes.
Filters
A filter string holds up to 8 filters separated by ;. Each filter is a dimension, an
operator, and up to 6 values separated by |. A query holds at most 16 values in total.
country==US|CA;page~/blog;browser!=Safari| Operator | Matches |
|---|---|
== | The value is one of the values |
!= | The value is none of the values. Rows with no value match too, unless (none) is one of the values. |
~ | The value contains one of the values |
!~ | The value contains none of the values |
In == and !=, * is a wildcard: page==/blog/*. The value (none) matches rows
with no value, for example referrer==(none).
Dimensions: page, entry_page, exit_page, hostname, event, goal, source,
referrer, channel, utm_source, utm_medium, utm_campaign, utm_term,
utm_content, country, region, city, continent, timezone, network,
browser, browser_version, os, os_version, device, screen, language, and
prop:<name> for a custom property. A property name is 1 to 64 letters, digits, _,
., or -. GET /api/v1/meta/dimensions returns the full list as key and label
pairs, without goal. It is Public.
goal==Signup filters by the saved goal named Signup: its page or its event. One goal
filter can't mix page goals and event goals.
channel takes one of Direct, Organic Search, Paid Search, AI Assistants,
Organic Social, Paid Social, Email, Video, Affiliates, Display, and
Referral.
Stats endpoints
Every stats endpoint needs Viewer on the site.
GET /api/v1/stats/aggregate
The headline metrics for the period.
{
"current": { "visitors": 3361, "visits": 4286, "pageviews": 9332, "views_per_visit": 2.18, "bounce_rate": 48.9, "visit_duration": 96, "events": 1830, "revenue": 5192, "average_revenue": 40.9 },
"previous": { "visitors": 2453, "...": "..." },
"change": { "visitors": 37.2, "bounce_rate": 2.2, "...": "..." }
}visitors counts unique visitors per day. bounce_rate is a percentage of visits.
visit_duration is in seconds, from the first to the last pageview or custom event of a
visit. change is the percent change of each metric, except bounce_rate, which
changes in percentage points. previous and change are null without compare. The
response also has site and window (from and to in milliseconds).
GET /api/v1/stats/timeseries
All headline metrics per time bucket. Returns interval, window (from and to in
milliseconds), points, and previous (null without compare). Each point has t,
the bucket start in milliseconds, and the same fields as current above. Empty buckets
are returned as zeros.
GET /api/v1/stats/breakdown
The top values of one dimension. Extra parameters:
| Parameter | Values | Default |
|---|---|---|
property | A dimension, goal (conversions per goal), props (every custom property as key: value rows), or search_term (Google Search Console) | page |
limit | 1 to 200. Up to 1000 for search_term. | 10 |
page | 1 and up | 1 |
search | Only values that contain this text | None |
Returns property, rows, total_visitors, and has_more. Each row has key,
visitors, visits, pageviews, events, bounce_rate, and visit_duration. Rows of
page also have time_on_page (seconds) and scroll_depth (percent). Rows of
exit_page also have exit_rate. Rows of event, goal, props, and prop:<name>
also have conversion_rate, revenue, and average_revenue. Goal rows also have kind
and match. Rows of visit dimensions, such as source, have revenue and
average_revenue when the visits had revenue.
search_term rows have key, clicks, impressions, ctr (percent), position (the
average position, 1 is the top), and visitors, which holds the clicks. They come from
Google, live, and stay cached for ten minutes. For a site without
Search Console, search_term returns 409.
A response with truncated: true came from a shard that had more rows than its cap of
500, so the rarest rows may be missing.
GET /api/v1/stats/realtime
Visitors in the last 5 minutes, pageviews per minute for the last 30 minutes, the top
pages, sources, and countries right now, and a feed of recent events. It ignores period.
GET /api/v1/stats/vitals
For each Core Web Vital (lcp, inp, cls, fcp, ttfb): the 75th percentile, the
number of samples, and the counts rated good, needs improvement, and poor. LCP, INP, FCP,
and TTFB are in milliseconds.
POST /api/v1/stats/funnel
Runs a funnel over the period. The body takes the query parameters as JSON fields (site,
not site_id), plus steps (2 to 8) and mode:
{
"site": "shop",
"period": "30d",
"mode": "sequential",
"steps": [
{ "kind": "page", "match": "/" },
{ "kind": "page", "match": "/pricing" },
{ "kind": "event", "match": "Signup", "label": "Signed up" }
]
}Each step has kind (page or event), match (a path or an event name, cut to 200
characters), and an optional label (cut to 80). In place of steps, funnel takes the
id of a saved funnel and runs its steps and mode. A mode in the body
overrides the saved one.
| Mode | A visitor completes a step when |
|---|---|
sequential | The steps happen in order. Other pages and events may come between them. |
strict | The steps happen in order with nothing between them. |
flexible | The middle steps happen in order. The last step counts anyone who reached it after the first. |
A missing or unknown mode is sequential. Returns mode, entered, completed, and
steps. Each step has label, visitors, conversion (percent of the first step), and
dropoff.
GET /api/v1/stats/journeys
The next or previous page after a path. Extra parameters: path (comma-separated pages,
for example /,/pricing), direction (forward or backward), and limit (1 to 50,
default 12). Rows include (exit) and (entrance) for the ends of a visit. Reloads of the
same page count once.
GET /api/v1/stats/behavior
What people did on pages, from the engagement beacon
(tracking guide). Extra parameters: report
(required), page (one path without ; or |, which replaces any page filter), and
limit (default 50, at most 500 rows).
report | Returns |
|---|---|
clicks | pageviews, visitors, clicks, rage, dead, and elements: page, selector, label, clicks, visitors, rage, dead |
scroll | visits, average (percent), and reached: for each depth 0, 10 … 100, the visits that scrolled at least that far and their share |
search | searches, visitors, total_visitors, and terms: term, searches, visitors |
errors | count, visitors, and errors: message, source, count, visitors, last_seen, up to 5 pages and browsers |
forms | forms: form, starts, submissions, conversion, visitors, and fields in page order: field, interactions, dropoffs, position |
media | media: name, type, plays, visitors, and reached, the percent of plays that got to 25, 50, 75% and the end |
content | content: name, impressions, clicks, ctr, visitors |
curl -H "Authorization: Bearer $KEY" \
"https://app.misomoon.com/api/v1/stats/behavior?site=shop&period=30d&report=clicks&page=/pricing"A response with truncated: true came from a shard that had more rows than the cap, so
the rarest rows may be missing.
GET /api/v1/stats/export
A ZIP file with one CSV file per report for the period and filters. The file is named
misomoon_<site>_<from>_<to>.zip, with the first and last day of the period.
| File | Columns |
|---|---|
visitors.csv | date, visitors, visits, pageviews, views_per_visit, bounce_rate, visit_duration, events, revenue. One row per day, week, or month, from interval. minute and hour give days. |
pages.csv | name, visitors, pageviews, bounce_rate, time_on_page, scroll_depth |
entry_pages.csv | name, visitors, visits, bounce_rate, visit_duration |
exit_pages.csv | name, visitors, visits, exit_rate |
custom_events.csv | name, visitors, events, conversion_rate, revenue, average_revenue |
conversions.csv | name, visitors, events, conversion_rate, revenue, average_revenue, one row per goal. It holds the top 10 goals by visitors, or limit goals when the request sets limit (at most 200). |
channels.csv, sources.csv, referrers.csv, utm_sources.csv, utm_mediums.csv, utm_campaigns.csv, utm_contents.csv, utm_terms.csv, countries.csv, regions.csv, cities.csv, browsers.csv, browser_versions.csv, operating_systems.csv, operating_system_versions.csv, devices.csv, screens.csv, languages.csv | name, visitors, visits, pageviews, bounce_rate, visit_duration |
Each breakdown file holds at most 1000 rows.
Stats API v2
POST /api/v2/query takes one JSON query and returns rows. It supports no dimension, one
time dimension, or one property dimension. It needs Viewer on the site.
{
"site_id": "shop",
"metrics": ["visitors", "pageviews", "bounce_rate"],
"date_range": "7d",
"dimensions": ["visit:source"],
"filters": [["is", "visit:country", ["US", "CA"]]],
"order_by": [["visitors", "desc"]],
"pagination": { "limit": 20, "offset": 0 }
}| Field | Values |
|---|---|
site_id | Required. A site id or the site's domain. |
metrics | Required. Any field of the aggregate, timeseries, or breakdown rows, such as visitors, visits, pageviews, views_per_visit, bounce_rate, visit_duration, events, revenue, conversion_rate, or time_on_page. total_revenue reads revenue, and percentage and group_conversion_rate read conversion_rate. A metric the rows lack returns null. |
date_range | Required. One of day (today), 24h, 7d, 28d, 30d, 91d (the 90d period), month (month to date), 6mo, 12mo, year (year to date), and all, or a pair of days such as ["2026-01-01", "2026-01-31"]. Any other name returns 400. |
dimensions | At most one. time, or time:minute, time:hour, time:day, time:week, or time:month, for one row per bucket. Or one property dimension from the list below. |
filters | A list of [operator, dimension, values]. ["and", [filter, ...]] groups filters. Every filter applies. |
order_by | A list of [metric or dimension, "asc" or "desc"]. The first pair sorts first. |
include | imports: false leaves imported history out. total_rows: true adds meta.total_rows. time_labels: true adds meta.time_labels for a time dimension. |
pagination | limit (1 to 10000, default 10000) and offset (default 0). |
Property dimensions: event:page, event:hostname, event:name, event:goal,
visit:entry_page, visit:exit_page, visit:source, visit:referrer,
visit:channel, visit:utm_source, visit:utm_medium, visit:utm_campaign,
visit:utm_content, visit:utm_term, visit:device, visit:browser,
visit:browser_version, visit:os, visit:os_version, visit:country,
visit:region, visit:city, visit:screen, and event:props:<name>.
visit:country_name, visit:region_name, and visit:city_name return the same values
as visit:country, visit:region, and visit:city. A property dimension returns at most
the top 200 values by visitors.
Filter operators are is, is_not, contains, contains_not, matches_wildcard, and
matches_wildcard_not. Filters take the same dimensions. matches_wildcard is is with
* as a wildcard, as in Filters.
The response is {"results": [{"dimensions": [...], "metrics": [...]}], "meta": {...}, "query": {...}}.
metrics follow the order of the query's metrics. A time dimension's label is the
bucket start in UTC, as YYYY-MM-DD HH:MM:SS for time:minute and time:hour and as
YYYY-MM-DD otherwise.
Sites
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites | Any | Lists the sites you can read, each with your role on it. Without a token, the public sites with role set to null. With a shared link, its one site, also with role set to null. |
POST /api/v1/sites | Session | Adds a site. See Add a site. Returns 201 and the site. |
GET /api/v1/sites/:id | Viewer | One site. |
PATCH /api/v1/sites/:id | Editor | Changes name, timezone, currency, public, features, search_params, include_paths, exclude_paths, exclude_ips, privacy_signals, or retention_days. features is the list of what the tracker collects beyond pageviews: any of outbound, downloads, forms, events, payments, vitals, clicks, errors, fields, media, content, search (Choose what to track). search_params lists up to 10 URL parameters that hold the site search term. An empty list restores q, s, search, query, keyword. include_paths and exclude_paths take up to 20 paths each, starting with /, where * matches within one part of the path and ** across. An empty list removes them. exclude_ips takes up to 50 IPv4 or IPv6 addresses or CIDR ranges, such as 203.0.113.7 or 2001:db8::/32, whose visits never count; an empty list or null removes them. privacy_signals is true to drop visits from browsers that send Global Privacy Control or Do Not Track. retention_days is a whole number of days from 30 to 3650 after which the site's events and visits are deleted, or null to keep them as long as the server's RETENTION_DAYS does (Privacy settings). The site object carries the lists as comma-separated strings, null for the defaults. Returns the site. |
DELETE /api/v1/sites/:id | Admin | Deletes the site and all of its data on every shard, and its managed proxy hostname. Returns 204. |
GET /api/v1/sites/:id/usage | Viewer | The site's daily ingest cap and what it stored per UTC day over the last 31 days: {"cap": 500000, "blocked": false, "days": [{"day": "2026-10-23", "events": 812, "stored": 1544, "dropped": 0, "capped": false}]}. cap is null without a cap. events counts pageviews and custom events, and stored counts every row, engagement pings too. A public dashboard or a shared link doesn't open it. |
POST /api/v1/sites/:id/demo | Admin | Writes synthetic traffic through the real write path, for local development. Off unless DEMO_SEED=1, and 403 otherwise. The optional body takes days (default 35, at most 120), perDay (default 140, at most 2000), and seed (default 7). Returns {"site": ..., "events": <rows written>}. |
A site object has id, domain, name, timezone, currency, team_id, public
(0 or 1), ingest_cap, features, search_params, include_paths,
exclude_paths, exclude_ips, privacy_signals (0 or 1), retention_days, and
created_at in milliseconds.
Add a site
POST /api/v1/sites takes:
| Field | Meaning |
|---|---|
domain | Required. The server lowercases it and strips http://, https://, www., and any path. A port, such as localhost:5173, stays. |
id | 2 to 40 characters of a-z, 0-9, and -. Without it, the id comes from the domain: each run of other characters becomes -, for example shop-example-com. |
name | At most 80 characters. Defaults to the domain. |
timezone | An IANA name. Defaults to UTC. |
currency | An ISO 4217 code. Defaults to USD. |
public | true lets anyone read the stats without a token. |
team | The team that owns the site. You need to be its owner, admin, or editor, or the request returns 403. |
Without team, the site goes to the first team you joined as owner, admin, or editor. If
you have none, the server makes a team named My sites with you as owner. API keys can't
add sites. A domain or id that is already taken returns 409.
Goals
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/goals | Viewer | Lists the goals: id, site_id, name, kind, match. |
POST /api/v1/sites/:id/goals | Editor | Adds a goal. Returns 201 and the goal. |
DELETE /api/v1/sites/:id/goals/:goal | Editor | Deletes a goal. Returns 204. |
The body takes kind (event or page, required), match (required: the event name,
or the page path, where * is a wildcard), and name. match is cut to 200 characters.
name is cut to 80 and defaults to match. A site has at most 50 goals, and the 51st
returns 400. A name the site already uses returns 409.
Funnels
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/funnels | Viewer | Lists the saved funnels: id, name, steps, mode. |
POST /api/v1/sites/:id/funnels | Editor | Saves a funnel. Returns 201 with its id. |
PUT /api/v1/sites/:id/funnels/:funnel | Editor | Replaces a funnel's name, steps, and mode. 404 for an unknown funnel. |
DELETE /api/v1/sites/:id/funnels/:funnel | Editor | Deletes a funnel. Returns 204, or 404 for an unknown funnel. |
The body takes name (required, cut to 80 characters), steps (at least 2, and only the
first 8 are kept), and mode (sequential, strict, or flexible, default
sequential). Steps take the same fields as in
POST /api/v1/stats/funnel.
Segments
A segment is a saved filter string. A site segment is shared with everyone who can read the site. A personal segment is visible to its author only, so a viewer can save one.
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/segments | Viewer | Lists the site's segments and your personal ones, by name: id, name, filters, personal (0 or 1). |
POST /api/v1/sites/:id/segments | Editor, or Viewer with a session for a personal segment | Saves a segment. Returns 201 with id, name, filters, and personal. |
DELETE /api/v1/sites/:id/segments/:segment | Editor for a site segment, the author for a personal one | Deletes a segment. Returns 204, also for a segment that doesn't exist. |
The body takes name (required, cut to 80 characters), filters (required, a
filter string), and personal (true for a personal segment). A segment
with the same name and owner gets the new filters and keeps its id.
Annotations
Notes on the chart, such as deploys.
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/annotations | Viewer | Lists up to 500 annotations by time: id, ts, text, source. from and to are milliseconds. from defaults to 0, to to a day from now. |
POST /api/v1/sites/:id/annotations | Editor | Adds an annotation. Returns 201 and the annotation. |
DELETE /api/v1/sites/:id/annotations/:annotation | Editor | Deletes an annotation. Returns 204. |
The body takes text (required, cut to 200 characters), ts (milliseconds or an ISO
date, default now), and source (cut to 40 characters, default manual). An API key
whose owner is an editor may post one, so CI can mark deploys:
curl -X POST "https://analytics.example.com/api/v1/sites/shop/annotations" \
-H "Authorization: Bearer $MISOMOON_KEY" -H "Content-Type: application/json" \
-d '{"text": "Deployed v2.3.0", "source": "ci"}'Shared links
A shared link opens a read-only dashboard of one site, with an optional password, segment, and expiry.
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/shared | Editor | Lists the links: slug, name, protected, segment_id, expires_at, created_at. |
POST /api/v1/sites/:id/shared | Editor | Makes a link. Returns 201 with slug, name, protected, segment_id, and expires_at. |
DELETE /api/v1/sites/:id/shared/:slug | Editor | Deletes a link. Returns 204. |
GET /api/v1/share/:slug | Public | {"name": ..., "protected": true}, so the page knows to ask for a password. 404 for a link that expired or was removed. |
POST /api/v1/share/:slug | Public | Trades the link, and its password when it has one, for a sh_ token: {"token": ..., "site": ..., "name": ..., "expires_at": ...}. The token lasts 7 days, or until the link expires if that is sooner. A wrong password returns 401. |
The body of POST /api/v1/sites/:id/shared takes name (cut to 80 characters, default
Shared link), password, segment_id (a segment applied to every query through the
link), and expires_days (at most 3650). Without expires_days, the link never expires.
Email reports and alerts
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/reports | Editor | {"subscriptions": [...], "sending": true}. Each subscription has id, email, kind, threshold, last_sent_at. sending is false without an email provider. |
POST /api/v1/sites/:id/reports | Editor | Subscribes an email. Returns 201 with {"ok": true}. |
DELETE /api/v1/sites/:id/reports/:report | Editor | Deletes a subscription. Returns 204. |
GET /api/v1/sites/:id/reports/preview | Editor | The report as it would go out now, as HTML. kind is weekly (default) or monthly. format=pdf returns the PDF attachment instead, which needs the BROWSER binding (501 without it, 503 when the print fails). |
POST /api/v1/sites/:id/reports/test | Editor | Sends the report now to email (required), with kind weekly (default) or monthly. Returns {"ok": true}, or 503 when the email could not go out. |
The body of POST /api/v1/sites/:id/reports takes email (required), kind
(required), and threshold, a whole number. A threshold out of range returns 400. A
second subscription for the same email and kind replaces the threshold.
kind | What it sends | threshold |
|---|---|---|
weekly | The last full week, Monday to Sunday, in the site's time zone | Not stored |
monthly | The last full month | Not stored |
spike | An alert when the visitors online in the last 5 minutes reach the threshold. Checked every 5 minutes, at most once in 12 hours. | Visitors online, 1 or more. Default 10. |
drop | An alert when the site has had no pageview for the threshold, if it had pageviews the week before. Checked hourly, sent once until traffic returns. | Hours without a pageview, 1 to 168. Default 12. |
Imports
Imported history from a CSV export or a Google Analytics property. Unfiltered reports add it to the site's own data. Every route needs Editor.
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/imports | Editor | Lists the imports, newest first: id, site_id, source (csv or google), label, from_day, to_day, status (importing, done, or failed), error, rows, created_at. |
POST /api/v1/sites/:id/imports | Editor | Imports a CSV export. Returns 201 and the import. |
DELETE /api/v1/sites/:id/imports/:import | Editor | Deletes the import and its rows on every shard. Returns 204. |
POST /api/v1/sites/:id/imports/google/connect | Editor | Starts Google sign-in. Returns {"url": ...}, the Google consent page. |
GET /api/v1/sites/:id/imports/google/properties | Editor | The Google Analytics properties the signed-in Google account can read, for grant=<grant id>: id (properties/123), name, account. |
POST /api/v1/sites/:id/imports/google | Editor | Starts a Google import in the background. Returns 202 with {"id": ..., "status": "importing"}. |
POST /api/v1/sites/:id/imports takes multipart/form-data with one or more file
fields: a ZIP of the export, or its CSV files. A file counts when its name is
imported_<table>.csv or imported_<table>_YYYYMMDD_YYYYMMDD.csv. The tables read are
visitors, sources, pages, entry_pages, exit_pages, locations, browsers,
operating_systems, devices, and custom_events. A request with no such file returns
400. While the site's history moves between shards, an import returns 409. Try again
a few hours later.
For a Google import, open the URL from POST /api/v1/sites/:id/imports/google/connect.
Google returns the person to the dashboard settings with google=<grant id> in the URL.
Google sign-in must finish within an hour of the connect call. POST /api/v1/sites/:id/imports/google
takes grant (required), property (required, properties/123), from and to
(YYYY-MM-DD, default the earliest day Google serves and yesterday in UTC), and
label. The server deletes the grant when the import finishes. Without Google configured,
these routes return 501.
Google Search Console
The connected Search Console property feeds property=search_term in
GET /api/v1/stats/breakdown. Every route needs Editor.
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/search-console | Editor | {"available": true, "connected": {"property": ..., "created_at": ..., "connected_by": <name>}}. available is false without Google configured. connected is null until a property is connected. |
POST /api/v1/sites/:id/search-console/connect | Editor | Starts Google sign-in. Returns {"url": ...}. Google returns the person to the dashboard settings with search=<grant id>. |
GET /api/v1/sites/:id/search-console/properties | Editor | The properties the Google account can read, for grant=<grant id>: property and matches (whether it is the site's domain). Matching properties come first. Unverified ones are left out. |
PUT /api/v1/sites/:id/search-console | Editor | Connects a property. Takes grant and property, which must be one of the listed properties. Returns the same object as GET. |
DELETE /api/v1/sites/:id/search-console | Editor | Disconnects the property and revokes the Google token. Returns 204. |
Payment webhooks
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/revenue | Editor | {"sources": [...], "last_30_days": {"payments": 12, "attributed": 9}}. Each source has provider, webhook_url, created_at, last_event_at, last_test_at, and last_error. |
PUT /api/v1/sites/:id/revenue/:provider | Editor | Connects a provider: stripe, polar, dodo, lemonsqueezy, or custom. Returns provider and webhook_url. Also turns on the site's payments feature. |
DELETE /api/v1/sites/:id/revenue/:provider | Editor | Disconnects a provider. Returns 204. |
POST /api/webhooks/:provider/:site | Public, signed by the provider | Takes the provider's webhook. Returns {"ok": true, "counted": 2}, or {"ok": true, "test": true, "counted": 0} for a test event. |
PUT takes {"secret": "whsec_..."}, the signing secret, which stripe, polar, and
dodo require. For lemonsqueezy and custom, a secret is optional and needs at least
16 characters. Without one, the server makes a secret and returns it once as secret.
An unknown provider returns 404.
The webhook returns 404 for an unknown provider or a site without that webhook, 413
for a body over 256 KB, 401 when the signature does not match the saved secret, and
400 for a body it can't read. Connect payments lists the events each
provider sends and the body for custom.
Managed proxy
The site's own subdomain, such as stats.example.com, pointed at misomoon with a CNAME
(Serve the tracker from your own domain). Every route needs Editor.
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/sites/:id/proxy | Editor | {"available": true, "target": <CNAME target>, "domain": {...}}. domain has hostname, status, ssl_status, active, records (the DNS records to add: the CNAME and any TXT records), and script (the tracker URL on the hostname). It is null without a hostname. While the hostname is not active, each call refreshes its state from Cloudflare. |
PUT /api/v1/sites/:id/proxy | Editor | Sets the hostname: {"hostname": "stats.example.com"}. It must be a subdomain of the site's domain, other than www. A new hostname replaces the old one. Returns the same object as GET. |
DELETE /api/v1/sites/:id/proxy | Editor | Removes the hostname. Returns the same object as GET, with domain set to null. |
Without Cloudflare for SaaS configured, PUT returns 501 and available is false.
A Cloudflare API error returns 502.
Accounts and teams
Roles, from most to least access: owner, admin, editor, viewer, and billing. Architecture lists what each role can do.
Sign-in
| Method and path | Role | What it does |
|---|---|---|
POST /api/v1/auth/signup | Public | Creates an account and a team. |
POST /api/v1/auth/login | Public | Starts a browser session. Takes email and password. Returns token, expires_at, and user. A wrong email or password returns 401. |
POST /api/v1/auth/logout | Any | Ends the ms_ session in the header. Returns 204. |
POST /api/v1/auth/forgot | Public | Emails a password reset link for email. |
POST /api/v1/auth/reset | Public | Sets a new password from a reset link. |
POST /api/v1/auth/signup takes email, password, and name (cut to 80 characters,
default the part of the email before @). It creates a team named <name>'s sites with
you as owner, and returns 201 with token, expires_at, and user. An email that
already has an account returns 409. Whether sign-up is open depends on SIGNUPS
(Variables):
SIGNUPS | Sign-up |
|---|---|
unset or open | Open to anyone. |
invite | Open only while the server has no account, so the first person can claim it. After that it returns 403, and people join through an invitation. |
closed | Always 403, the first account included. Invitations still work. |
A password has 10 to 200 characters. POST /api/v1/auth/forgot always returns
{"ok": true, "sending": ...}, whether or not the email has an account. sending is
false without an email provider. The link works for an hour and replaces any earlier
link. POST /api/v1/auth/reset takes token, the code from the link, and password. It
signs out every other session of the account and returns a new token and expires_at.
An expired link returns 400.
Your account
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/me | Any | Who you are, and what you can open. |
PATCH /api/v1/me | Session | Changes name (cut to 80 characters), or password with current_password. Returns id, email, and name. A wrong current_password returns 401. |
DELETE /api/v1/me | Session | Deletes your account. Takes {"password": "..."}; a wrong or missing password returns 401. The teams only you are in are deleted with their sites and all of their data; you leave your other teams, lose guest access, and your sessions, API keys, saved segments, pending invitations and email reports go too. If you are the only owner of a team with other members, it returns 409 until you make one of them an owner. Returns 204. |
GET /api/v1/me returns:
| Field | Meaning |
|---|---|
kind | user, key, share, or anonymous |
user | id, email, and name of the signed-in user or the key's owner, or null |
teams | Your teams with id, name, and your role. Empty except for a browser session. |
sites | The same list as GET /api/v1/sites |
demo | The public demo site's id (DEMO_SITE), or null |
signups | Whether POST /api/v1/auth/signup is open |
imported | The ids of your sites with a finished import |
search | The ids of your sites with Search Console connected |
share | For a shared link, slug and segment (its filter string). Otherwise null. |
API keys
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/keys | Session | Your keys, newest first: id, name, prefix, site_id, created_at, last_used_at. |
POST /api/v1/keys | Session | Makes a key. Takes name (cut to 80 characters, default API key) and site, which limits the key to one site you can read. Returns 201 with id, name, key, prefix, and site_id. key is shown once. |
POST /api/v1/keys/:key/rotate | Session | Gives a key a new secret and returns it once. The name and site stay, and the old secret stops working at once. |
DELETE /api/v1/keys/:key | Session | Deletes a key. Returns 204. |
Teams and members
| Method and path | Role | What it does |
|---|---|---|
GET /api/v1/teams | Session | Your teams: id, name, your role, and the number of members and sites. |
POST /api/v1/teams | Session | Makes a team with you as owner. Takes name (cut to 80 characters, default My team). Returns 201 with id, name, created_at, and role. |
PATCH /api/v1/teams/:team | Session, team owner or admin | Renames the team. Takes name. |
DELETE /api/v1/teams/:team | Session, team owner | Deletes the team. A team that still has sites returns 409. Returns 204. |
GET /api/v1/teams/:team/members | Session, team owner or admin | {"members": [...], "guests": [...], "invitations": [...]}. Members have id, email, name, role, created_at. Guests of the team's sites have id, email, name, role, site_id. Pending invitations have id, email, role, site_id, expires_at, created_at. |
PATCH /api/v1/teams/:team/members/:user | Session, team owner or admin | Changes a member's role to owner, admin, editor, viewer, or billing. |
DELETE /api/v1/teams/:team/members/:user | Session, team owner or admin, or the member themselves | Removes a member, or leaves the team. Returns 204. |
POST /api/v1/sites/:id/transfer | Session, owner of the site's team | Moves the site to team, where you need to be owner or admin. Guests who are members of the new team stop being guests, and the site's pending invitations are deleted. |
A :user who is not on the team returns 404. An admin can't make an owner, change
an owner's role, or remove an owner (403). A change that would leave the team without an
owner returns 409.
Invitations
| Method and path | Role | What it does |
|---|---|---|
POST /api/v1/teams/:team/invitations | Session, team owner or admin. For a guest, Admin on site. | Invites email and returns 201 with id, email, role, site_id, url, and sent. |
DELETE /api/v1/teams/:team/invitations/:invitation | Session, team owner or admin | Withdraws an invitation. Returns 204. |
GET /api/v1/invitations/:token | Public | What the invitation is for: email, role, team, site, invited_by, and has_account. 404 for an invitation that expired or was withdrawn. |
POST /api/v1/invitations/:token/accept | Public, or Session as the invited email | Joins the team, or the site as a guest. Returns site (the guest site, or the team's first site) and team. |
POST /api/v1/teams/:team/invitations takes email, role, and site:
- Without
site, it invites a team member. It needs a session that is team owner or admin, androleis a team role. Only an owner can invite an owner. An email that is already on the team returns409. - With
site, it invites a guest to that one site. It needs Admin on the site, which must belong to the team, androleiseditororviewer.
A new invitation for the same email and site replaces the earlier one. The link in url
works for 7 days. The server emails it and sets sent to whether the email went out, so
the link can be passed on another way.
To accept, a person signed in with the invited email sends no body. Signed in with another
email, the request returns 403. Without a session, the body takes name and password,
and the server creates the account for the invited email and also returns token and
expires_at.
Guests
| Method and path | Role | What it does |
|---|---|---|
PATCH /api/v1/sites/:id/guests/:user | Admin | Changes a guest's role to editor or viewer. |
DELETE /api/v1/sites/:id/guests/:user | Admin, or the guest themselves | Removes a guest from the site, or leaves it. Returns 204. |
Event ingestion
POST /api/event takes one event as JSON. The tracker sends it as text/plain so the
browser makes no preflight request. The response is always 202, even for an event that
is dropped, so the endpoint reveals nothing about which sites exist. Events past a site's
daily cap are dropped too (Daily ingest cap). It is
Public. OPTIONS /api/event answers a preflight with 204.
| Field | Meaning |
|---|---|
s | Site id. Required. |
n | Event name: pageview, engagement, or a custom name of 1 to 120 characters, without control characters, semicolons, or vertical bars. Without it, a beacon with e is an engagement and one with w a pageview, as m.js sends them. |
u | The full page URL, http or https. Required. Its hostname must be the site's domain or a subdomain of it. |
r | document.referrer |
t | document.title |
w, h, vw | Screen width and height, viewport width |
l | navigator.language. Without it, the request's Accept-Language |
p | Custom properties: an object of strings, numbers, and booleans. The server keeps the first 30 whose keys are 1 to 64 letters, digits, _, ., or -, and cuts strings to 300 characters. |
$ | Revenue: { "a": 19, "c": "USD" }, or { "amount": 19, "currency": "USD" } |
k | Payment keys: a ref, or a list of checkout ids. At most 4 are kept. |
i | false keeps a custom event from ending a bounce |
e, d, v | Engagement only: engaged milliseconds, deepest scroll percent, and Web Vitals |
q | Pageview only: the site search term. Kept only when the site tracks search. Without it, the server reads the term from the page URL's search parameters. |
c, x, f, g, m, o | Engagement only: what happened on the page since the last beacon, as lists of arrays. c clicks [selector, label, clicks, rage, dead] (at most 30), x errors [message, source, count] (5), f forms [form, started, submitted] (10), g form fields [form, field, position, touched, dropped] (50), m media [name, type, played, 25%, 50%, 75%, 95%] (10), and o content [name, seen, clicked] (20). Numbers are counts since the last beacon. |
ts | Client timestamp in milliseconds. The server uses it when it is within 5 minutes of the server's clock, and its own time otherwise. |
id | A UUID for the event. A second event with the same id is not stored, so a client can resend after a lost response. The server picks an id when it is missing or not a UUID. |
The server removes emails, ids, tokens, and numbers of 6 or more digits from q and from
the text in the interaction lists. Form fields carry names and flags, never values.
Pageviews and engagements on a path that the site's include_paths or exclude_paths
leave out are dropped.
Bodies over 16 KB are dropped. Bots are dropped by Cloudflare's bot signals and by user-agent patterns.
A server that sends events for a visitor sets X-Misomoon-Ip to the visitor's address
and User-Agent to the visitor's user agent. X-Misomoon-Country, -Region, -City,
-Continent, and -Timezone carry the location, URL-encoded. These are the headers a
first-party proxy sets. An app sends a user agent of the form
MisomoonApp/<sdk version> (<platform>) <app name>/<app version>, and reports show the
app name in place of the browser. The SDK guide covers both.
GET /api/health returns 200 with {"ok": true} when the Worker is up.
Tracker files
| Method and path | Role | What it does |
|---|---|---|
GET /m.js | Public | The tracker. ?s=<site id> adds the code for the features the site turned on in its settings. Without a known site, it is the core alone, which counts nothing. Browsers cache it for 5 minutes with s and for an hour without. |
GET /overlay.js | Public | Draws clicks and scroll depth on the site's own page. m.js loads it only on a page opened with #misomoon-overlay in the URL. Browsers cache it for an hour. |
On a managed proxy hostname, the Worker serves /m.js, /overlay.js, and /api/event
only. Every other path returns 404.
MCP endpoint
POST /mcp takes JSON-RPC 2.0 requests from MCP clients, with the same bearer tokens as
the API. A batch holds at most 20 requests, and a body over 64 KB returns 413. GET /mcp
returns 405, because the server does not stream. Ask AI tools about your traffic lists the tools and
the client setup.