Overview
Read MIRA FIVE projects, reports, people and events from a script with the read-only REST API and an API key.
The REST API serves the numbers the MIRA FIVE dashboard shows: projects, the overview, people, raw events, goals, funnels and acquisition. Use it from scripts and servers, for example a nightly export or an internal report. It only reads. To let an AI agent read and set things up, use the MCP server instead.
Base URL
https://app.mirafive.io/api/v1Every endpoint answers GET and HEAD. Any other method gets 405 Method Not Allowed with Allow: GET, HEAD. Unknown query parameters are ignored.
Authenticate
Send a credential as a bearer token:
Authorization: Bearer mf_pat_…Two kinds work:
| Credential | Looks like | Lifetime |
|---|---|---|
| API key | mf_pat_, eight lower-case letters or digits, _, then 40 letters or digits | 30 days, 90 days, 1 year or never, chosen when you create it |
| OAuth access token | issued to an app you approved (see MCP server) | 1 hour, renewed with a 30-day refresh token |
Create an API key as described in Keys. The examples on these pages read it from MIRAFIVE_API_KEY.
A credential acts as its owner. It sees every project in every organization you belong to, and loses a project the moment you leave its organization. The REST API is read-only whatever the key's switches allow.
An API key reads everything you can read, people included. Keep it on the server, in an environment variable. Never ship it to a browser or commit it.
Make a first request
curl https://app.mirafive.io/api/v1/projects \
-H "Authorization: Bearer $MIRAFIVE_API_KEY"{
"data": [
{
"id": "01932c4e-8a7b-7c3d-9e2f-4b5a6c7d8e9f",
"type": "projects",
"attributes": {
"name": "Nordlicht Shop",
"slug": "nordlicht-shop",
"timezone": "Europe/Berlin",
"reportingCurrency": "EUR",
"archived": false,
"createdAt": "2025-11-04T09:12:44.318+01:00",
"organization": {
"id": "01932c4d-1f2e-7a3b-8c4d-5e6f7a8b9c0d",
"slug": "nordlicht",
"name": "Nordlicht GmbH"
},
"plan": "pro",
"retentionDays": 366
},
"links": {
"self": "https://app.mirafive.io/api/v1/projects/01932c4e-8a7b-7c3d-9e2f-4b5a6c7d8e9f"
}
}
],
"links": {
"self": "https://app.mirafive.io/api/v1/projects",
"next": null
},
"meta": {
"perPage": 50,
"nextCursor": null
}
}Every other endpoint sits under a project: /api/v1/projects/{project_id}/…. The project_id is the id from this list, a UUID. A slug gets 404.
Response format
Successful answers are JSON:API documents with Content-Type: application/vnd.api+json, whatever Accept header you send. URLs and names are written unescaped (no \/).
dataholds one resource object, or a list of them. Each hasid,type,attributesand, where it has an address,links.self.metaholds what the answer is about: the period it read, the page size, totals.- Lists that page have top-level
links(self,next). See Pagination. - There are no relationships and no
include.
Sparse fieldsets
Ask for only some attributes with fields[<type>], a comma-separated list:
curl -G https://app.mirafive.io/api/v1/projects/01932c4e-8a7b-7c3d-9e2f-4b5a6c7d8e9f \
-H "Authorization: Bearer $MIRAFIVE_API_KEY" \
--data-urlencode 'fields[projects]=name,timezone'{
"data": {
"id": "01932c4e-8a7b-7c3d-9e2f-4b5a6c7d8e9f",
"type": "projects",
"attributes": { "name": "Nordlicht Shop", "timezone": "Europe/Berlin" },
"links": { "self": "https://app.mirafive.io/api/v1/projects/01932c4e-8a7b-7c3d-9e2f-4b5a6c7d8e9f" }
}
}Timestamps
Moments (createdAt, firstSeen, occurredAt, …) are ISO 8601 with milliseconds, in the project's timezone with its offset: 2026-09-23T13:55:00.000+02:00. The bounds in meta.period and the start of each trend bucket (t) are ISO 8601 without milliseconds: 2026-09-28T00:00:00+02:00.
Money and ratios
Amounts stay per currency and are never added across currencies: [{ "currency": "EUR", "amount": 49 }, { "currency": "CHF", "amount": 25 }]. Ratios such as rate, conversion, coverage and ctr are between 0 and 1.
Periods
Reports read a period. Pass a preset, or period=custom with two days.
| Name | Type | Default | Description |
|---|---|---|---|
period | string | 30d | 1h, 24h, 7d, 30d, 90d, 12m or custom. The events endpoint takes only 1h, 24h and 7d, and defaults to 24h. |
from | string | none | First day, YYYY-MM-DD, in the project's timezone. Required with period=custom, refused otherwise. |
to | string | none | Last day, inclusive, YYYY-MM-DD. Required with period=custom, refused otherwise. |
A preset ends with the current minute, hour, day or month and includes it: 7d on 27 September is 21 to 27 September, today included. Days and months are cut in the project's timezone.
Rules for a custom range:
tomust be on or afterfrom.fromafter today (in the project's timezone) is refused. Atoafter today reads up to today.- The plan keeps a limited history (
retentionDayson the project). A range that starts before it starts at the first day the plan keeps instead, andmeta.period.clampedToRetentionistrue. A range that ends before it is refused, for example withto is before 2025-09-26, the first day the plan keeps.
Trends are bucketed by the period:
| Period | Bucket (interval) |
|---|---|
1h | minute |
24h | hour |
7d, 30d, 90d | day |
12m | month |
custom | day up to 120 days, week beyond |
Every report says which window it read in meta.period:
"period": {
"preset": "30d",
"from": "2026-08-29T00:00:00+02:00",
"to": "2026-09-28T00:00:00+02:00",
"timezone": "Europe/Berlin",
"interval": "day",
"comparison": null,
"clampedToRetention": false
}to is exclusive. comparison holds the from and to of the previous window when the answer compares, else null.
Compare with the previous period
compare=previous adds the window before it, of the same length. Only three endpoints take it: the overview, a goal report and search. Everywhere else it is refused with This endpoint does not compare periods. Leave out compare.
The overview's key numbers and the search summary carry the previous values even without compare. A previous window that starts before the history the plan keeps, or before the project's first event, is left out: comparison is then null.
Filters
The overview, events and channels endpoints narrow their events with filters. Each filter is three query parameters with the same index. All filters must hold (AND). At most 20.
filters[0][field]=page_path&filters[0][op]=starts_with&filters[0][value]=/productsWith curl, let -G and --data-urlencode encode the brackets:
curl -G https://app.mirafive.io/api/v1/projects/01932c4e-8a7b-7c3d-9e2f-4b5a6c7d8e9f/overview \
-H "Authorization: Bearer $MIRAFIVE_API_KEY" \
--data-urlencode 'period=7d' \
--data-urlencode 'filters[0][field]=channel' \
--data-urlencode 'filters[0][op]=is' \
--data-urlencode 'filters[0][value]=organic_search' \
--data-urlencode 'filters[1][field]=country' \
--data-urlencode 'filters[1][op]=is' \
--data-urlencode 'filters[1][value]=DE'field is at most 150 characters, op 20, value 1,000. The People list has its own filters on people.
Fields
| Field | Operators | Value |
|---|---|---|
event_name | text | e.g. checkout_started, $pageview |
page_path | text | e.g. /products/linen-shirt |
page_host | text | host without www., compared in lower case |
referrer_host | text | host without www., compared in lower case |
utm_source, utm_medium, utm_campaign, utm_term, utm_content | text | as tagged |
region | text | |
channel | is, is_not | paid_search, paid_social, email, ai_assistant, organic_search, organic_social, campaign, referral, direct |
country | closed set | two-letter code, e.g. DE |
click_source, device_type, browser, os, locale, source_type, collection_mode | closed set | as the events carry it |
properties.<key> | all | a custom event property, e.g. properties.plan. The key matches ^[A-Za-z0-9_$][A-Za-z0-9_.$-]{0,127}$. |
"text" means every operator except gt and lt. "closed set" means is, is_not, set and not_set.
channel is the channel of the visit the event belongs to. A visit collected without consent keeps only its first page, so under a channel filter it counts by that page. A project that collects only without consent refuses a channel filter: This project collects without consent, so it is not filtered by channel.
Operators
| Operator | Matches when the field | Value |
|---|---|---|
is | equals the value | required |
is_not | differs from the value | required |
contains | contains the value, ignoring case | required |
not_contains | does not contain the value, ignoring case | required |
starts_with | starts with the value | required |
matches | matches the pattern; * is the only wildcard | required |
set | is not empty | none |
not_set | is empty | none |
gt | is a number greater than the value | a number, e.g. 49 |
lt | is a number less than the value | a number |
Pagination
Lists of projects, people and events come 50 at a time and page by cursor. There is no page number and no page size parameter.
"links": {
"self": "https://app.mirafive.io/api/v1/projects/01932c4e-8a7b-7c3d-9e2f-4b5a6c7d8e9f/events",
"next": "https://app.mirafive.io/api/v1/projects/01932c4e-8a7b-7c3d-9e2f-4b5a6c7d8e9f/events?cursor=eyJhdCI6IjIwMjYtMDktMjcgMDg6MTQ6MDIuNTEyIn0"
},
"meta": {
"perPage": 50,
"nextCursor": "eyJhdCI6IjIwMjYtMDktMjcgMDg6MTQ6MDIuNTEyIn0"
}Follow links.next until it is null, or pass meta.nextCursor as cursor with the same other parameters. A cursor the list did not hand out gets 422 with The cursor is not one this list handed out; follow links.next.
Rate limits
Each person has 120 requests per minute, shared by all their API keys and tokens. The MCP server has a budget of its own. Every answer carries the budget:
RateLimit-Policy: "api";q=120;w=60
RateLimit: "api";r=117;t=42r is the requests left, t the seconds until the window resets. Past the limit the answer is 429 with Retry-After and The limit is 120 requests per minute. Try again in 42 seconds.
Refused tokens are limited too: 60 per minute from one network. Past that, a bad token gets 429 with Too many refused tokens came from this network. Try again in N seconds. A valid token from the same network still gets through.
Browsers and CORS
The API sends no CORS headers, so a page in a browser cannot read it from another origin. Call it from a server or a script.
Every answer carries X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: strict-origin-when-cross-origin and Content-Security-Policy: default-src 'none'; frame-ancestors 'none'.
Audit trail
Every call is recorded under API & MCP in MIRA FIVE as recent access: the key or app, the endpoint, the project and how the call ended. Query parameters are never stored, since they can name people. A call with a revoked or expired key is recorded against its owner, so a leaked key shows up there.
Endpoints
| Endpoint | Page |
|---|---|
GET /api/v1/projects | Projects |
GET /api/v1/projects/{project_id} | Projects |
GET /api/v1/projects/{project_id}/overview | Overview report |
GET /api/v1/projects/{project_id}/people | People |
GET /api/v1/projects/{project_id}/people/{ref} | People |
GET /api/v1/projects/{project_id}/events | Events |
GET /api/v1/projects/{project_id}/goals | Goals |
GET /api/v1/projects/{project_id}/goals/{goal_id} | Goals |
GET /api/v1/projects/{project_id}/funnel | Funnel |
GET /api/v1/projects/{project_id}/acquisition/channels | Acquisition |
GET /api/v1/projects/{project_id}/acquisition/search | Acquisition |
GET /api/v1/projects/{project_id}/acquisition/ads | Acquisition |
There is no OpenAPI document. Errors are listed in Errors.