API
The public Trakily REST API.
The Trakily API gives your own code the numbers from your dashboard: visitors, pageviews, revenue, goals, breakdowns by channel or country, realtime visitors, subscription metrics and launches. It also accepts custom events and payments from your server, for things that happen outside the browser or providers we don't sync.
It's a JSON REST API at https://trakily.com/api/v1, authenticated with per-website API keys. API access is included in the Scale plan and the free trial.
Authentication
Send your key as a Bearer token in the Authorization header on every request. Requests without a valid key get a 401.
Authorization: Bearer trk_your_api_key
A key can read all of a website's analytics and revenue, and read-write keys can add data. Store keys in environment variables, never in browser code, mobile apps or public repositories. The API sends no CORS headers, so browsers can't call it directly.
API keys
Website owners create keys in Settings → API. Each key belongs to one website and only sees that website. Give each integration its own key so you can revoke one without breaking the others.
- Read-only keys can call every
GETendpoint. - Read & write keys can also send events (
POST /events) and payments (POST /payments). - The full key (
trk_followed by 40 characters) is shown once, when you create it. We store only a hash, so a lost key can't be recovered: create a new one. - The list shows each key's first characters, its permissions and when it was last used.
- Revoking a key takes effect immediately. Revoked keys can then be deleted. A website can have up to 25 keys.
Plans
API access is included in the Scale plan and during the 14-day free trial, even though the trial otherwise runs with Growth limits. After the trial, only Scale includes it: if the plan behind a website changes to one without API access, or the account becomes inactive, its keys get a 403 (plan_required or account_locked) until it's on Scale again. Keys also stop working while the website's only owner has deleted their account (it's restored with the account). See Billing and limits.
Requests and responses
- Successful responses are JSON with a
dataobject; lists that page also have apaginationobject. - Money is in the website's reporting currency (in
currency), in major units:49.99means $49.99. Payments in other currencies are converted with daily ECB rates, the same way the dashboard does it. Revenue is net of refunds. - Rates are fractions:
0.0093is 0.93 %. Durations are seconds. - Periods and chart buckets follow the website's timezone. Timestamps are ISO 8601 in UTC; buckets also carry a
locallabel in the website's timezone. - Unknown query parameters are rejected with a
400, so a typo never silently returns unfiltered data. - Numbers match the dashboard: the API uses the same queries, including excluded countries and imported history (GA4, Plausible) for visitors, sessions, bounce rate and duration. Pageviews count Trakily-tracked data only.
Pagination
/stats/breakdown and /goals take limit (1–1000, default 100) and page (from 1). The response says where you are:
{
"pagination": {
"page": 1,
"limit": 100,
"total": 240,
"has_more": true,
"next_page": 2
}
}Rate limits
Each key can make 120 requests per minute, counted in fixed one-minute windows across all endpoints. Every authenticated response says where you stand:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute (120). |
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | When the window resets, in Unix seconds. |
Retry-After | On 429 responses: seconds to wait before retrying. |
- Header
X-RateLimit-Limit- Meaning
- Requests allowed per minute (120).
- Header
X-RateLimit-Remaining- Meaning
- Requests left in the current window.
- Header
X-RateLimit-Reset- Meaning
- When the window resets, in Unix seconds.
- Header
Retry-After- Meaning
- On 429 responses: seconds to wait before retrying.
Over the limit you get a 429 with code rate_limited. Wait for Retry-After seconds, then retry. Need more? Use one key per integration.
Errors
Every error has the same shape. details lists each invalid parameter when there is more than one thing to fix.
{
"error": {
"code": "invalid_request",
"message": "granularity: Not available for this period. Use day or week.",
"details": [
{
"param": "granularity",
"message": "Not available for this period. Use day or week."
}
]
}
}| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A parameter or body field is missing, unknown or invalid. |
| 401 | missing_api_key | No Authorization header. |
| 401 | invalid_api_key | The header is malformed or the key doesn't exist. |
| 401 | revoked_api_key | The key was revoked. |
| 403 | insufficient_scope | A read-only key called a write endpoint. |
| 403 | plan_required | The website's plan doesn't include API access. |
| 403 | account_locked | The website's Trakily plan is inactive. |
| 403 | collection_paused | Writes only: collection is paused for the website (billing grace period over, account deleted, or tracking paused in its settings). |
| 404 | not_found | Unknown endpoint, or a website other than the key's. |
| 413 | payload_too_large | Request body over 16 KB. |
| 415 | unsupported_media_type | Body isn't sent as application/json. |
| 429 | rate_limited | Too many requests this minute. |
| 500 | internal_error | Something failed on our side. Safe to retry. |
- Status
- 400
- Code
invalid_request- When
- A parameter or body field is missing, unknown or invalid.
- Status
- 401
- Code
missing_api_key- When
- No Authorization header.
- Status
- 401
- Code
invalid_api_key- When
- The header is malformed or the key doesn't exist.
- Status
- 401
- Code
revoked_api_key- When
- The key was revoked.
- Status
- 403
- Code
insufficient_scope- When
- A read-only key called a write endpoint.
- Status
- 403
- Code
plan_required- When
- The website's plan doesn't include API access.
- Status
- 403
- Code
account_locked- When
- The website's Trakily plan is inactive.
- Status
- 403
- Code
collection_paused- When
- Writes only: collection is paused for the website (billing grace period over, account deleted, or tracking paused in its settings).
- Status
- 404
- Code
not_found- When
- Unknown endpoint, or a website other than the key's.
- Status
- 413
- Code
payload_too_large- When
- Request body over 16 KB.
- Status
- 415
- Code
unsupported_media_type- When
- Body isn't sent as
application/json.
- Status
- 429
- Code
rate_limited- When
- Too many requests this minute.
- Status
- 500
- Code
internal_error- When
- Something failed on our side. Safe to retry.
GET /sites/{id}
The key's website: domain, timezone, reporting currency and connected payment providers. {id} is the website's id, public id (pls_…) or domain.
curl "https://trakily.com/api/v1/sites/example.com" \ -H "Authorization: Bearer trk_your_api_key"
const res = await fetch("https://trakily.com/api/v1/sites/example.com", {
headers: { Authorization: `Bearer ${process.env.TRAKILY_API_KEY}` },
});
const { data } = await res.json();{
"data": {
"id": "219e0982-904a-4bf1-9c02-7f53e8510896",
"public_id": "pls_examplecom000000001",
"domain": "example.com",
"name": "example.com",
"timezone": "Europe/Berlin",
"currency": "EUR",
"tracking_mode": "cookies",
"allowed_hostnames": [
"app.example.com"
],
"payment_providers": [
{
"provider": "stripe",
"status": "active"
}
],
"first_event_at": "2025-10-07T00:09:26.254Z",
"created_at": "2025-10-05T16:47:20.453Z"
}
}GET /stats/summary
Totals for a period: the numbers at the top of your dashboard, plus pageviews and sessions.
| Parameter | Type | Description |
|---|---|---|
period | string | One of now, today, yesterday, 24h, 7d, 30d, 3m, 6m, 12m, wtd, mtd, ytd, all. Default 30d. Ranges end now and follow the website's timezone. Like the dashboard, ranges start no earlier than the history the plan shows (Starter 1 year, Growth 3, Scale 5): the response's period.visible_since gives that start and period.history_limited is true when your range was cut. |
from | date | Start of a custom range, YYYY-MM-DD, inclusive. Use with to instead of period (at most 1,900 days). |
to | date | End of a custom range, YYYY-MM-DD, inclusive. |
offset | integer | Shift the period back this many periods (0–500). period=7d&offset=1 is the 7 days before the last 7. |
filter | string, repeatable | Narrow to sessions matching dimension:value, e.g. filter=country:US. Values are the value keys the breakdown endpoint returns. Operators go after the dimension: country!:US (is not), page~:/blog (contains, case-insensitive), page!~:/blog (does not contain). Up to 14; all must match. Responses echo each filter as { dimension, operator, value }. Imported Google Analytics or Plausible history is included only when the filters can be answered from it (all on one imported dimension); the response's imported object says whether it was (included), up to which day, which metrics it adds to and, in breakdowns, whether the rows include it (in_rows). It's null when the period has no imported data. |
site_id | string | Optional. The website's id, public id or domain. Keys only work for their own website; anything else is a 404. |
compare | boolean | Add previous (the period before) and change (relative change, 0.25 = +25 %, null when the previous value was 0). |
- Parameter
period- Type
- string
- Description
- One of
now,today,yesterday,24h,7d,30d,3m,6m,12m,wtd,mtd,ytd,all. Default30d. Ranges end now and follow the website's timezone. Like the dashboard, ranges start no earlier than the history the plan shows (Starter 1 year, Growth 3, Scale 5): the response'speriod.visible_sincegives that start andperiod.history_limitedistruewhen your range was cut.
- Parameter
from- Type
- date
- Description
- Start of a custom range,
YYYY-MM-DD, inclusive. Use withtoinstead ofperiod(at most 1,900 days).
- Parameter
to- Type
- date
- Description
- End of a custom range,
YYYY-MM-DD, inclusive.
- Parameter
offset- Type
- integer
- Description
- Shift the period back this many periods (0–500).
period=7d&offset=1is the 7 days before the last 7.
- Parameter
filter- Type
- string, repeatable
- Description
- Narrow to sessions matching
dimension:value, e.g.filter=country:US. Values are thevaluekeys the breakdown endpoint returns. Operators go after the dimension:country!:US(is not),page~:/blog(contains, case-insensitive),page!~:/blog(does not contain). Up to 14; all must match. Responses echo each filter as{ dimension, operator, value }. Imported Google Analytics or Plausible history is included only when the filters can be answered from it (all on one imported dimension); the response'simportedobject says whether it was (included), up to which day, whichmetricsit adds to and, in breakdowns, whether the rows include it (in_rows). It'snullwhen the period has no imported data.
- Parameter
site_id- Type
- string
- Description
- Optional. The website's id, public id or domain. Keys only work for their own website; anything else is a 404.
- Parameter
compare- Type
- boolean
- Description
- Add
previous(the period before) andchange(relative change,0.25= +25 %,nullwhen the previous value was 0).
| Metric | Meaning |
|---|---|
visitors | Unique visitors. |
pageviews | Pageviews. |
sessions | Visits. |
bounce_rate | Share of sessions with one pageview and under 10 seconds of engagement. |
avg_session_duration | Average engaged time per session, in seconds. |
revenue | Revenue net of refunds, in the reporting currency. |
customers | New paying customers (first payments). |
conversion_rate | customers ÷ visitors. |
revenue_per_visitor | revenue ÷ visitors. |
- Metric
visitors- Meaning
- Unique visitors.
- Metric
pageviews- Meaning
- Pageviews.
- Metric
sessions- Meaning
- Visits.
- Metric
bounce_rate- Meaning
- Share of sessions with one pageview and under 10 seconds of engagement.
- Metric
avg_session_duration- Meaning
- Average engaged time per session, in seconds.
- Metric
revenue- Meaning
- Revenue net of refunds, in the reporting currency.
- Metric
customers- Meaning
- New paying customers (first payments).
- Metric
conversion_rate- Meaning
- customers ÷ visitors.
- Metric
revenue_per_visitor- Meaning
- revenue ÷ visitors.
curl "https://trakily.com/api/v1/stats/summary?period=30d&compare=true" \ -H "Authorization: Bearer trk_your_api_key"
const res = await fetch("https://trakily.com/api/v1/stats/summary?period=30d&compare=true", {
headers: { Authorization: `Bearer ${process.env.TRAKILY_API_KEY}` },
});
const { data } = await res.json();{
"data": {
"site_id": "219e0982-904a-4bf1-9c02-7f53e8510896",
"period": {
"name": "30d",
"from": "2026-09-09T00:00:00.000Z",
"to": "2026-10-08T09:12:44.120Z",
"timezone": "UTC",
"visible_since": "2023-10-08T09:12:44.120Z",
"history_limited": false
},
"currency": "USD",
"filters": [],
"metrics": {
"visitors": 7879,
"pageviews": 16719,
"sessions": 8390,
"bounce_rate": 0.3369,
"avg_session_duration": 115.9,
"revenue": 9092.02,
"customers": 73,
"conversion_rate": 0.0093,
"revenue_per_visitor": 1.15
},
"previous": {
"period": {
"from": "2026-08-10T00:00:00.000Z",
"to": "2026-09-09T00:00:00.000Z",
"timezone": "UTC"
},
"metrics": {
"visitors": 6861,
"pageviews": 15080,
"sessions": 7600,
"bounce_rate": 0.3507,
"avg_session_duration": 114.1,
"revenue": 6447.1,
"customers": 50,
"conversion_rate": 0.0073,
"revenue_per_visitor": 0.94
}
},
"change": {
"visitors": 0.1484,
"pageviews": 0.1087,
"sessions": 0.1039,
"bounce_rate": -0.0393,
"avg_session_duration": 0.0158,
"revenue": 0.4102,
"customers": 0.46,
"conversion_rate": 0.274,
"revenue_per_visitor": 0.2234
}
}
}GET /stats/timeseries
Metrics per hour, day, week or month, for charts. Buckets cover the whole period; the current one is marked partial.
| Parameter | Type | Description |
|---|---|---|
period | string | One of now, today, yesterday, 24h, 7d, 30d, 3m, 6m, 12m, wtd, mtd, ytd, all. Default 30d. Ranges end now and follow the website's timezone. Like the dashboard, ranges start no earlier than the history the plan shows (Starter 1 year, Growth 3, Scale 5): the response's period.visible_since gives that start and period.history_limited is true when your range was cut. |
from | date | Start of a custom range, YYYY-MM-DD, inclusive. Use with to instead of period (at most 1,900 days). |
to | date | End of a custom range, YYYY-MM-DD, inclusive. |
offset | integer | Shift the period back this many periods (0–500). period=7d&offset=1 is the 7 days before the last 7. |
filter | string, repeatable | Narrow to sessions matching dimension:value, e.g. filter=country:US. Values are the value keys the breakdown endpoint returns. Operators go after the dimension: country!:US (is not), page~:/blog (contains, case-insensitive), page!~:/blog (does not contain). Up to 14; all must match. Responses echo each filter as { dimension, operator, value }. Imported Google Analytics or Plausible history is included only when the filters can be answered from it (all on one imported dimension); the response's imported object says whether it was (included), up to which day, which metrics it adds to and, in breakdowns, whether the rows include it (in_rows). It's null when the period has no imported data. |
site_id | string | Optional. The website's id, public id or domain. Keys only work for their own website; anything else is a 404. |
granularity | string | minute (period now only), hour, day, week or month. Each period allows a sensible subset (e.g. 30d: day or week); the error lists the allowed ones. Defaults to the dashboard's choice. |
metrics | comma list | Any of visitors, new_visitors, pageviews, sessions, bounce_rate, avg_session_duration, revenue, refunds, customers, conversion_rate. Default: all. |
- Parameter
period- Type
- string
- Description
- One of
now,today,yesterday,24h,7d,30d,3m,6m,12m,wtd,mtd,ytd,all. Default30d. Ranges end now and follow the website's timezone. Like the dashboard, ranges start no earlier than the history the plan shows (Starter 1 year, Growth 3, Scale 5): the response'speriod.visible_sincegives that start andperiod.history_limitedistruewhen your range was cut.
- Parameter
from- Type
- date
- Description
- Start of a custom range,
YYYY-MM-DD, inclusive. Use withtoinstead ofperiod(at most 1,900 days).
- Parameter
to- Type
- date
- Description
- End of a custom range,
YYYY-MM-DD, inclusive.
- Parameter
offset- Type
- integer
- Description
- Shift the period back this many periods (0–500).
period=7d&offset=1is the 7 days before the last 7.
- Parameter
filter- Type
- string, repeatable
- Description
- Narrow to sessions matching
dimension:value, e.g.filter=country:US. Values are thevaluekeys the breakdown endpoint returns. Operators go after the dimension:country!:US(is not),page~:/blog(contains, case-insensitive),page!~:/blog(does not contain). Up to 14; all must match. Responses echo each filter as{ dimension, operator, value }. Imported Google Analytics or Plausible history is included only when the filters can be answered from it (all on one imported dimension); the response'simportedobject says whether it was (included), up to which day, whichmetricsit adds to and, in breakdowns, whether the rows include it (in_rows). It'snullwhen the period has no imported data.
- Parameter
site_id- Type
- string
- Description
- Optional. The website's id, public id or domain. Keys only work for their own website; anything else is a 404.
- Parameter
granularity- Type
- string
- Description
minute(periodnowonly),hour,day,weekormonth. Each period allows a sensible subset (e.g.30d: day or week); the error lists the allowed ones. Defaults to the dashboard's choice.
- Parameter
metrics- Type
- comma list
- Description
- Any of
visitors,new_visitors,pageviews,sessions,bounce_rate,avg_session_duration,revenue,refunds,customers,conversion_rate. Default: all.
curl "https://trakily.com/api/v1/stats/timeseries?period=7d&granularity=day&metrics=visitors,revenue" \ -H "Authorization: Bearer trk_your_api_key"
const res = await fetch("https://trakily.com/api/v1/stats/timeseries?period=7d&granularity=day&metrics=visitors,revenue", {
headers: { Authorization: `Bearer ${process.env.TRAKILY_API_KEY}` },
});
const { data } = await res.json();{
"data": {
"site_id": "219e0982-904a-4bf1-9c02-7f53e8510896",
"period": {
"name": "7d",
"from": "2026-10-02T00:00:00.000Z",
"to": "2026-10-08T09:12:44.120Z",
"timezone": "UTC"
},
"granularity": "day",
"currency": "USD",
"filters": [],
"metrics": [
"visitors",
"revenue"
],
"points": [
{
"start": "2026-10-02T00:00:00.000Z",
"local": "2026-10-02",
"partial": false,
"visitors": 257,
"revenue": 205.51
},
{
"start": "2026-10-03T00:00:00.000Z",
"local": "2026-10-03",
"partial": false,
"visitors": 206,
"revenue": 87
},
{
"start": "2026-10-08T00:00:00.000Z",
"local": "2026-10-08",
"partial": true,
"visitors": 98,
"revenue": 49
}
]
}
}GET /stats/breakdown
Visitors, revenue and new customers per channel, referrer, page, country and more, sorted by visitors. Revenue is credited to the session that brought the buyer, the same way the dashboard does it.
| Parameter | Type | Description |
|---|---|---|
dimension | string, required | channel, referrer, campaign, page, entry, exit, hostname, country, region, city, browser, os, device, goal. |
period | string | One of now, today, yesterday, 24h, 7d, 30d, 3m, 6m, 12m, wtd, mtd, ytd, all. Default 30d. Ranges end now and follow the website's timezone. Like the dashboard, ranges start no earlier than the history the plan shows (Starter 1 year, Growth 3, Scale 5): the response's period.visible_since gives that start and period.history_limited is true when your range was cut. |
from | date | Start of a custom range, YYYY-MM-DD, inclusive. Use with to instead of period (at most 1,900 days). |
to | date | End of a custom range, YYYY-MM-DD, inclusive. |
offset | integer | Shift the period back this many periods (0–500). period=7d&offset=1 is the 7 days before the last 7. |
filter | string, repeatable | Narrow to sessions matching dimension:value, e.g. filter=country:US. Values are the value keys the breakdown endpoint returns. Operators go after the dimension: country!:US (is not), page~:/blog (contains, case-insensitive), page!~:/blog (does not contain). Up to 14; all must match. Responses echo each filter as { dimension, operator, value }. Imported Google Analytics or Plausible history is included only when the filters can be answered from it (all on one imported dimension); the response's imported object says whether it was (included), up to which day, which metrics it adds to and, in breakdowns, whether the rows include it (in_rows). It's null when the period has no imported data. |
site_id | string | Optional. The website's id, public id or domain. Keys only work for their own website; anything else is a 404. |
limit | integer | Rows per page, 1–1000 (default 100). |
page | integer | Page number, from 1. |
- Parameter
dimension- Type
- string, required
- Description
channel,referrer,campaign,page,entry,exit,hostname,country,region,city,browser,os,device,goal.
- Parameter
period- Type
- string
- Description
- One of
now,today,yesterday,24h,7d,30d,3m,6m,12m,wtd,mtd,ytd,all. Default30d. Ranges end now and follow the website's timezone. Like the dashboard, ranges start no earlier than the history the plan shows (Starter 1 year, Growth 3, Scale 5): the response'speriod.visible_sincegives that start andperiod.history_limitedistruewhen your range was cut.
- Parameter
from- Type
- date
- Description
- Start of a custom range,
YYYY-MM-DD, inclusive. Use withtoinstead ofperiod(at most 1,900 days).
- Parameter
to- Type
- date
- Description
- End of a custom range,
YYYY-MM-DD, inclusive.
- Parameter
offset- Type
- integer
- Description
- Shift the period back this many periods (0–500).
period=7d&offset=1is the 7 days before the last 7.
- Parameter
filter- Type
- string, repeatable
- Description
- Narrow to sessions matching
dimension:value, e.g.filter=country:US. Values are thevaluekeys the breakdown endpoint returns. Operators go after the dimension:country!:US(is not),page~:/blog(contains, case-insensitive),page!~:/blog(does not contain). Up to 14; all must match. Responses echo each filter as{ dimension, operator, value }. Imported Google Analytics or Plausible history is included only when the filters can be answered from it (all on one imported dimension); the response'simportedobject says whether it was (included), up to which day, whichmetricsit adds to and, in breakdowns, whether the rows include it (in_rows). It'snullwhen the period has no imported data.
- Parameter
site_id- Type
- string
- Description
- Optional. The website's id, public id or domain. Keys only work for their own website; anything else is a 404.
- Parameter
limit- Type
- integer
- Description
- Rows per page, 1–1000 (default 100).
- Parameter
page- Type
- integer
- Description
- Page number, from 1.
Each row has a value (the raw key, usable in filter) and a readable label. Countries are ISO codes (US); regions and cities are country|name (US|California); ?? means unknown. With dimension=goal, rows also have completions, and revenue is what visitors who completed the goal paid in the period.
curl "https://trakily.com/api/v1/stats/breakdown?dimension=channel&period=30d&limit=2" \ -H "Authorization: Bearer trk_your_api_key"
const res = await fetch("https://trakily.com/api/v1/stats/breakdown?dimension=channel&period=30d&limit=2", {
headers: { Authorization: `Bearer ${process.env.TRAKILY_API_KEY}` },
});
const { data } = await res.json();{
"data": {
"site_id": "219e0982-904a-4bf1-9c02-7f53e8510896",
"period": {
"name": "30d",
"from": "2026-09-09T00:00:00.000Z",
"to": "2026-10-08T09:12:44.120Z",
"timezone": "UTC",
"visible_since": "2023-10-08T09:12:44.120Z",
"history_limited": false
},
"dimension": "channel",
"currency": "USD",
"filters": [],
"total_visitors": 7879,
"rows": [
{
"value": "Social",
"label": "Social",
"visitors": 5291,
"revenue": 5610,
"customers": 45
},
{
"value": "Direct",
"label": "Direct",
"visitors": 1132,
"revenue": 632,
"customers": 7
}
]
},
"pagination": {
"page": 1,
"limit": 2,
"total": 6,
"has_more": true,
"next_page": 2
}
}GET /realtime
Visitors active in the last 5 minutes, newest first. limit (0–200, default 50) caps the recent list; visitors is always the full count.
curl "https://trakily.com/api/v1/realtime?limit=1" \ -H "Authorization: Bearer trk_your_api_key"
const res = await fetch("https://trakily.com/api/v1/realtime?limit=1", {
headers: { Authorization: `Bearer ${process.env.TRAKILY_API_KEY}` },
});
const { data } = await res.json();{
"data": {
"site_id": "219e0982-904a-4bf1-9c02-7f53e8510896",
"window_minutes": 5,
"visitors": 14,
"customers": 1,
"recent": [
{
"visitor_id": "krhbxpdrd9bn70lxvatiekq05knv998d",
"customer": false,
"customer_name": null,
"page": "/pricing",
"referrer": "news.ycombinator.com",
"country": "DE",
"city": "Berlin",
"browser": "Chrome",
"device": "desktop",
"session_started_at": "2026-10-08T09:10:02.000Z"
}
]
}
}GET /goals
Goals completed in the period with completions, unique visitors, conversion rate and the revenue from those visitors. Takes the period and filter parameters plus limit and page. payment is the built-in goal for first payments.
curl "https://trakily.com/api/v1/goals?period=7d" \ -H "Authorization: Bearer trk_your_api_key"
const res = await fetch("https://trakily.com/api/v1/goals?period=7d", {
headers: { Authorization: `Bearer ${process.env.TRAKILY_API_KEY}` },
});
const { data } = await res.json();{
"data": {
"site_id": "219e0982-904a-4bf1-9c02-7f53e8510896",
"period": {
"name": "7d",
"from": "2026-10-02T00:00:00.000Z",
"to": "2026-10-08T09:12:44.120Z",
"timezone": "UTC"
},
"currency": "USD",
"filters": [],
"goals": [
{
"name": "signup",
"label": "Sign up",
"completions": 76,
"visitors": 76,
"conversion_rate": 0.0578,
"revenue": 412
},
{
"name": "payment",
"label": "Payment",
"completions": 12,
"visitors": 12,
"conversion_rate": 0.0091,
"revenue": 1203.5
}
]
},
"pagination": {
"page": 1,
"limit": 100,
"total": 2,
"has_more": false,
"next_page": null
}
}GET /saas
Subscription metrics from your connected payment provider: MRR, ARR, churn, retention, LTV and the MRR movements per bucket. Takes the period parameters, granularity and compare. Filters can narrow customers by first touch with channel, referrer or country only. When no provider is connected and there's no payment data, connected is false and metrics is null.
curl "https://trakily.com/api/v1/saas?period=30d" \ -H "Authorization: Bearer trk_your_api_key"
const res = await fetch("https://trakily.com/api/v1/saas?period=30d", {
headers: { Authorization: `Bearer ${process.env.TRAKILY_API_KEY}` },
});
const { data } = await res.json();{
"data": {
"site_id": "219e0982-904a-4bf1-9c02-7f53e8510896",
"connected": true,
"period": {
"name": "30d",
"from": "2026-09-09T00:00:00.000Z",
"to": "2026-10-08T09:12:44.120Z",
"timezone": "UTC",
"visible_since": "2023-10-08T09:12:44.120Z",
"history_limited": false
},
"granularity": "day",
"currency": "USD",
"filters": [],
"metrics": {
"mrr": 2794.28,
"arr": 33531.36,
"net_new_mrr": 819.66,
"active_subscribers": 105,
"arpu": 26.61,
"customer_churn_rate": 0.0526,
"revenue_churn_rate": 0.0841,
"net_revenue_retention": 0.9261,
"ltv": 488.37,
"ltv_capped": false,
"trial_conversion_rate": 0.5,
"refund_rate": 0.0274
},
"mrr_series": [
{
"start": "2026-10-07T00:00:00.000Z",
"local": "2026-10-07",
"partial": false,
"mrr": 2760.28,
"new": 58,
"expansion": 0,
"reactivation": 0,
"contraction": 0,
"churn": -29,
"net": 29
}
],
"plans": [
{
"name": "Pro",
"interval": "month",
"subscribers": 71,
"trialing": 4,
"mrr": 2059
}
]
}
}GET /export
The same ZIP as Settings → Export: daily stats and every breakdown, raw events, goal completions and payments as CSV files. See Exporting your data for the columns. The response is application/zip, streamed as it's built; errors are the usual JSON.
| Parameter | Type | Description |
|---|---|---|
from | date | Required. First day, YYYY-MM-DD in the website's timezone, inclusive. |
to | date | Required. Last day, inclusive. Dates outside the history the plan shows are left out; the X-Export-From and X-Export-To headers give the dates actually exported. |
parts | string | Comma-separated: stats, events, payments, goals. Default: all. |
site_id | string | Optional, as on the other endpoints. |
- Parameter
from- Type
- date
- Description
- Required. First day,
YYYY-MM-DDin the website's timezone, inclusive.
- Parameter
to- Type
- date
- Description
- Required. Last day, inclusive. Dates outside the history the plan shows are left out; the
X-Export-FromandX-Export-Toheaders give the dates actually exported.
- Parameter
parts- Type
- string
- Description
- Comma-separated:
stats,events,payments,goals. Default: all.
- Parameter
site_id- Type
- string
- Description
- Optional, as on the other endpoints.
One request can hold up to 100,000 raw rows and 400 days of daily stats. Bigger exports return 413 payload_too_large: ask for fewer days at a time.
curl "https://trakily.com/api/v1/export?from=2026-09-01&to=2026-09-30&parts=events,payments" \ -H "Authorization: Bearer trk_your_api_key" \ -o trakily-september.zip
GET /launches
Every saved launch with its match rules, measured window, metrics (visitors, sign-ups, customers, revenue, revenue per visitor), a daily series for the days after it, its rank by revenue per visitor and the verdict line the dashboard shows. Newest first. Each launch has its own window, so the period and filter parameters don't apply. Takes site_id and suggestions=true to also list suggested launches found in mentions, UTM campaigns and referrer spikes. Launches are part of the Growth and Scale plans.
curl "https://trakily.com/api/v1/launches" \ -H "Authorization: Bearer trk_your_api_key"
const res = await fetch("https://trakily.com/api/v1/launches", {
headers: { Authorization: `Bearer ${process.env.TRAKILY_API_KEY}` },
});
const { data } = await res.json();{
"data": {
"site_id": "219e0982-904a-4bf1-9c02-7f53e8510896",
"currency": "USD",
"visible_since": "2023-10-08T09:12:44.120Z",
"best_launch_id": "5d0c2f7e-3a52-4c3b-9b8e-0f4f3c1d2a10",
"launches": [
{
"id": "5d0c2f7e-3a52-4c3b-9b8e-0f4f3c1d2a10",
"name": "Build-in-public thread on X",
"platform": "x",
"url": null,
"launched_at": "2026-09-14T00:03:11.970Z",
"window_days": 7,
"goal": "signup",
"rules": [
{
"type": "tco",
"code": "trjwnkcf7z"
}
],
"source": "mention",
"window": {
"from": "2026-09-14T00:03:11.970Z",
"to": "2026-09-21T00:03:11.970Z",
"in_progress": false,
"history_limited": false,
"outside_history": false
},
"metrics": {
"visitors": 253,
"signups": 11,
"customers": 1,
"revenue": 169,
"revenue_per_visitor": 0.67
},
"daily": [
{
"day": 1,
"start": "2026-09-14T00:03:11.970Z",
"visitors": 26,
"signups": 2,
"revenue": 0,
"partial": false
}
],
"rank": 1,
"verdict": "$0.67 per visitor. Your best launch. Do more of these."
}
]
}
}POST /events
Record a custom event or goal from your server, e.g. a signup confirmed by email or an action in a background job. Needs a read & write key. Events count toward your monthly event allowance and trigger goal alerts like browser events.
| Field | Type | Description |
|---|---|---|
name | string, required | Goal name, 1–64 characters, e.g. signup. |
props | object | Up to 10 properties. Keys: 1–32 characters of a-z 0-9 _ -. Values: strings (up to 255 characters), numbers or booleans (stored as text). |
visitor_id | string | The visitor's id, so the event is credited to the channel that brought them: the part of the _trakily_vid cookie before the first | (see Revenue). Your server can read it from the request cookies. |
session_id | string | Attach to a specific session. Must belong to visitor_id when both are sent. |
path | string | Page path to record, starting with /. Defaults to the session's last page. |
timestamp | date-time | When it happened (ISO 8601 with a timezone). Defaults to now; up to 7 days in the past. |
- Field
name- Type
- string, required
- Description
- Goal name, 1–64 characters, e.g. signup.
- Field
props- Type
- object
- Description
- Up to 10 properties. Keys: 1–32 characters of
a-z 0-9 _ -. Values: strings (up to 255 characters), numbers or booleans (stored as text).
- Field
visitor_id- Type
- string
- Description
- The visitor's id, so the event is credited to the channel that brought them: the part of the
_trakily_vidcookie before the first|(see Revenue). Your server can read it from the request cookies.
- Field
session_id- Type
- string
- Description
- Attach to a specific session. Must belong to visitor_id when both are sent.
- Field
path- Type
- string
- Description
- Page path to record, starting with /. Defaults to the session's last page.
- Field
timestamp- Type
- date-time
- Description
- When it happened (ISO 8601 with a timezone). Defaults to now; up to 7 days in the past.
With a visitor_id we know, the event joins that visitor's latest session and inherits its source, location and device (attributed: true). Without one, the event still counts toward the goal; anonymous events share one server-side visitor per day, shown as Direct traffic.
curl -X POST "https://trakily.com/api/v1/events" \
-H "Authorization: Bearer trk_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name":"signup","visitor_id":"krhbxpdrd9bn70lxvatiekq05knv998d","props":{"plan":"pro"}}'const res = await fetch("https://trakily.com/api/v1/events", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TRAKILY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "signup",
"visitor_id": "krhbxpdrd9bn70lxvatiekq05knv998d",
"props": {
"plan": "pro"
}
}),
});
const { data } = await res.json();{
"data": {
"accepted": true,
"name": "signup",
"visitor_id": "krhbxpdrd9bn70lxvatiekq05knv998d",
"session_id": "q2lk3x9a0b7cd8e1f4g5h6",
"attributed": true,
"timestamp": "2026-10-08T09:14:03.000Z"
}
}POST /payments
Send a payment or refund from a provider we don't sync (or your own billing). Needs a read & write key. Payments appear in revenue, customers and breakdowns exactly like synced ones and are matched to the visitor by visitor_id, session_id or the email of an identified visitor. If you use one of the supported providers, connect it instead so refunds and subscriptions sync on their own.
| Field | Type | Description |
|---|---|---|
external_id | string, required | Your id for this payment (order or invoice id), up to 200 characters. Makes the request idempotent. |
amount | number, required | Positive amount in major units: 49.99. No more decimals than the currency has (none for JPY). |
currency | string, required | ISO 4217 code, e.g. USD, EUR. |
type | string | new (default, a new customer), renewal or refund. |
email | string | Buyer's email; matched to visitors you identified with the same email. |
visitor_id | string | The buyer's visitor id: the most reliable way to attribute. |
session_id | string | The session the purchase happened in (the _trakily_sid cookie). |
customer_id | string | Your customer id. |
subscription_id | string | Groups renewals; renewals inherit the first payment's visitor. |
interval | string | one_time, day, week, month or year. |
refund_of | string | For refunds: external_id of the original payment (inherits its visitor). |
paid_at | date-time | When it was paid (ISO 8601). Defaults to now; up to 5 years back for imports. |
- Field
external_id- Type
- string, required
- Description
- Your id for this payment (order or invoice id), up to 200 characters. Makes the request idempotent.
- Field
amount- Type
- number, required
- Description
- Positive amount in major units:
49.99. No more decimals than the currency has (none for JPY).
- Field
currency- Type
- string, required
- Description
- ISO 4217 code, e.g.
USD,EUR.
- Field
type- Type
- string
- Description
new(default, a new customer),renewalorrefund.
- Field
email- Type
- string
- Description
- Buyer's email; matched to visitors you identified with the same email.
- Field
visitor_id- Type
- string
- Description
- The buyer's visitor id: the most reliable way to attribute.
- Field
session_id- Type
- string
- Description
- The session the purchase happened in (the
_trakily_sidcookie).
- Field
customer_id- Type
- string
- Description
- Your customer id.
- Field
subscription_id- Type
- string
- Description
- Groups renewals; renewals inherit the first payment's visitor.
- Field
interval- Type
- string
- Description
one_time,day,week,monthoryear.
- Field
refund_of- Type
- string
- Description
- For refunds: external_id of the original payment (inherits its visitor).
- Field
paid_at- Type
- date-time
- Description
- When it was paid (ISO 8601). Defaults to now; up to 5 years back for imports.
Idempotency
external_id identifies the payment. The first request creates it (201, duplicate: false). Any later request with the same external_id changes nothing and returns the stored payment (200, duplicate: true), so retrying after a timeout is always safe.
curl -X POST "https://trakily.com/api/v1/payments" \
-H "Authorization: Bearer trk_your_api_key" \
-H "Content-Type: application/json" \
-d '{"external_id":"order_1001","amount":49.99,"currency":"USD","email":"[email protected]","visitor_id":"krhbxpdrd9bn70lxvatiekq05knv998d"}'const res = await fetch("https://trakily.com/api/v1/payments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TRAKILY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"external_id": "order_1001",
"amount": 49.99,
"currency": "USD",
"email": "[email protected]",
"visitor_id": "krhbxpdrd9bn70lxvatiekq05knv998d"
}),
});
const { data } = await res.json();{
"data": {
"id": "937e5b75-70dc-488e-b6c6-bfc21e8b1fe0",
"external_id": "order_1001",
"type": "new",
"amount": 49.99,
"currency": "USD",
"email": "[email protected]",
"paid_at": "2026-10-08T09:14:30.273Z",
"visitor_id": "krhbxpdrd9bn70lxvatiekq05knv998d",
"session_id": "q2lk3x9a0b7cd8e1f4g5h6",
"attributed_by": "metadata",
"duplicate": false
}
}Versioning
The version is in the path: /api/v1. Within v1 we only make additive changes: new endpoints, new optional parameters and new fields in responses. Write clients that ignore fields they don't know. Anything that would break existing code (removing or renaming fields, changing meanings) ships as a new version, and v1 keeps working alongside it with advance notice before it's retired.