REST API
An HTTP API for your Statlark data — pull revenue, visitors and traffic sources into a script, spreadsheet or LLM, and push conversions, revenue and identity straight from your own server.
The base URL is https://app.statlark.com/api/v1. Every response is JSON. Every request needs a bearer token, and every token is scoped to a single site. Prefer machine-readable? There’s an OpenAPI 3.1 spec and an llms.txt index.
Authentication
Create a token under Settings → API keys for the site you want to read. The full token (slk_…) is shown once, at creation — copy it then; Statlark stores only a hash and can’t show it again. Lost it? Revoke it and create another.
Send it in the Authorizationheader on every request. Treat it like a password: it’s meant for server-side use — don’t embed it in a web page or app you ship to users.
Each token carries one or more permissions, chosen when you create it: analytics:read for the read endpoints below, and ingest:write for the write endpoints. A call needs a token with the matching permission or it returns 403.
curl "https://app.statlark.com/api/v1/overview?site=sl_xxxx&from=2026-06-01&to=2026-07-01" \
-H "Authorization: Bearer slk_your_token_here"Common parameters
Every read endpoint takes site. The ranged endpoints also take from/to; the “now” and metadata endpoints (realtime, site, and the goals list) take only site.
site— your site id (sl_…), the same value on your tracker snippet. Required. A token can only read the site it was created for; any other id returns403.fromandto— the window, as an ISO date (YYYY-MM-DD) or full timestamp. Required on the ranged endpoints. The range is interpreted in UTC and is half-open[from, to)—fromis included,tois not. The window can span at most 366 days per request.
Overview
GET /api/v1/overview — the headline metrics: revenue, visitors, paying conversion and revenue-per-visitor, each with the value for the preceding equal-length period (so you can compute a delta).
{
"site": "sl_xxxx",
"range": { "from": "2026-06-01T00:00:00.000Z", "to": "2026-07-01T00:00:00.000Z" },
"metrics": {
"revenue": 4820.5,
"previous_revenue": 3905,
"visitors": 12500,
"previous_visitors": 11000,
"paying_conversion": 0.02144,
"previous_paying_conversion": 0.019,
"revenue_per_visitor": 0.38564,
"previous_revenue_per_visitor": 0.355
}
}Revenue timeseries
GET /api/v1/timeseries— revenue over time, gap-filled into buckets in the site’s timezone. The optional bucket parameter is one of hour, day (default), week or month.
curl "https://app.statlark.com/api/v1/timeseries?site=sl_xxxx&from=2026-06-01&to=2026-06-04&bucket=day" \
-H "Authorization: Bearer slk_your_token_here"{
"site": "sl_xxxx",
"range": { "from": "2026-06-01T00:00:00.000Z", "to": "2026-06-04T00:00:00.000Z" },
"bucket": "day",
"points": [
{ "bucket_start": "2026-06-01T00:00:00+00:00", "revenue": 149.7 },
{ "bucket_start": "2026-06-02T00:00:00+00:00", "revenue": 0 },
{ "bucket_start": "2026-06-03T00:00:00+00:00", "revenue": 99.8 }
]
}Traffic sources
GET /api/v1/sources — your traffic sources ranked by revenue, each with visitors, revenue-per-visitor and paying conversion. The optional grouping parameter is one of channel (default), utm_source, referrer or adclick.
curl "https://app.statlark.com/api/v1/sources?site=sl_xxxx&from=2026-06-01&to=2026-07-01&grouping=channel" \
-H "Authorization: Bearer slk_your_token_here"{
"site": "sl_xxxx",
"range": { "from": "2026-06-01T00:00:00.000Z", "to": "2026-07-01T00:00:00.000Z" },
"grouping": "channel",
"sources": [
{
"source": "Organic",
"visitors": 3072,
"revenue": 1152,
"revenue_per_visitor": 0.375,
"paying_conversion": 0.0078125
}
]
}Breakdowns
GET /api/v1/breakdowns — audience breakdowns for the window: countries, cities, devices, browsers and hostnames, each row with visitors, attributed revenue and revenue-per-visitor. A dimension holds at most 10 rows: the top 8 keys by visitors (ties broken by revenue, then key), then an Unknown row for visitors with no value, then an Other row aggregating rank 9 and below. Unknown and Other always sort last, however large they are — and on devices the lower-case unknown key (an unreadable user agent) is a separate row from Unknown. (Per-row paying conversion isn’t available here — use sources for conversion.)
curl "https://app.statlark.com/api/v1/breakdowns?site=sl_xxxx&from=2026-06-01&to=2026-07-01" \
-H "Authorization: Bearer slk_your_token_here"{
"site": "sl_xxxx",
"range": { "from": "2026-06-01T00:00:00.000Z", "to": "2026-07-01T00:00:00.000Z" },
"totals": { "visitors": 6400, "revenue": 2480, "identified": 512, "customers": 48 },
"countries": [
{ "key": "US", "visitors": 3072, "revenue": 1536, "revenue_per_visitor": 0.5 },
{ "key": "GB", "visitors": 1024, "revenue": 384, "revenue_per_visitor": 0.375 },
{ "key": "DE", "visitors": 512, "revenue": 192, "revenue_per_visitor": 0.375 },
{ "key": "CA", "visitors": 384, "revenue": 96, "revenue_per_visitor": 0.25 },
{ "key": "FR", "visitors": 256, "revenue": 64, "revenue_per_visitor": 0.25 },
{ "key": "AU", "visitors": 192, "revenue": 48, "revenue_per_visitor": 0.25 },
{ "key": "RO", "visitors": 128, "revenue": 32, "revenue_per_visitor": 0.25 },
{ "key": "NL", "visitors": 96, "revenue": 24, "revenue_per_visitor": 0.25 },
{ "key": "Unknown", "visitors": 320, "revenue": 0, "revenue_per_visitor": 0 },
{ "key": "Other", "visitors": 416, "revenue": 104, "revenue_per_visitor": 0.25 }
],
"cities": [
{ "key": "New York", "visitors": 1024, "revenue": 512, "revenue_per_visitor": 0.5 },
{ "key": "London", "visitors": 768, "revenue": 288, "revenue_per_visitor": 0.375 },
{ "key": "San Francisco", "visitors": 512, "revenue": 320, "revenue_per_visitor": 0.625 },
{ "key": "Berlin", "visitors": 384, "revenue": 96, "revenue_per_visitor": 0.25 },
{ "key": "Bucharest", "visitors": 256, "revenue": 112, "revenue_per_visitor": 0.4375 },
{ "key": "Unknown", "visitors": 3456, "revenue": 1152, "revenue_per_visitor": 0.3333333333333333 }
],
"devices": [
{ "key": "desktop", "visitors": 3576, "revenue": 1344, "revenue_per_visitor": 0.37583892617449666 },
{ "key": "mobile", "visitors": 2560, "revenue": 1024, "revenue_per_visitor": 0.4 },
{ "key": "tablet", "visitors": 224, "revenue": 112, "revenue_per_visitor": 0.5 },
{ "key": "bot", "visitors": 24, "revenue": 0, "revenue_per_visitor": 0 },
{ "key": "unknown", "visitors": 8, "revenue": 0, "revenue_per_visitor": 0 },
{ "key": "Unknown", "visitors": 8, "revenue": 0, "revenue_per_visitor": 0 }
],
"browsers": [
{ "key": "Chrome", "visitors": 3200, "revenue": 1200, "revenue_per_visitor": 0.375 },
{ "key": "Safari", "visitors": 2048, "revenue": 896, "revenue_per_visitor": 0.4375 },
{ "key": "Firefox", "visitors": 640, "revenue": 240, "revenue_per_visitor": 0.375 },
{ "key": "Edge", "visitors": 448, "revenue": 112, "revenue_per_visitor": 0.25 },
{ "key": "Unknown", "visitors": 64, "revenue": 32, "revenue_per_visitor": 0.5 }
],
"hostnames": [
{ "key": "example.com", "visitors": 5120, "revenue": 2080, "revenue_per_visitor": 0.40625 },
{ "key": "app.example.com", "visitors": 1024, "revenue": 384, "revenue_per_visitor": 0.375 },
{ "key": "blog.example.com", "visitors": 256, "revenue": 16, "revenue_per_visitor": 0.0625 }
]
}Pages
GET /api/v1/pages — pages ranked by pageviews, each with visitors, entries and landing-attributed revenue + revenue-per-visitor. Set entry_only=true to restrict to entry (landing) pages. revenue_per_visitor divides that revenue by the page’s landing visitors, not by the row’s visitors, and is null for a page that is no range-active visitor’s landing page (rather than 0).
curl "https://app.statlark.com/api/v1/pages?site=sl_xxxx&from=2026-06-01&to=2026-07-01&entry_only=false" \
-H "Authorization: Bearer slk_your_token_here"{
"site": "sl_xxxx",
"range": { "from": "2026-06-01T00:00:00.000Z", "to": "2026-07-01T00:00:00.000Z" },
"entry_only": false,
"pages": [
{
"path": "/pricing",
"pageviews": 4100,
"visitors": 2600,
"entries": 900,
"revenue": 1899.5,
"revenue_per_visitor": 1.8264423076923078
},
{
"path": "/blog/revenue-attribution",
"pageviews": 2380,
"visitors": 1740,
"entries": 1310,
"revenue": 412.25,
"revenue_per_visitor": 0.2998181818181818
},
{
"path": "/docs/api",
"pageviews": 960,
"visitors": 610,
"entries": 0,
"revenue": 0,
"revenue_per_visitor": null
}
]
}Goals
GET /api/v1/goals with no goal returns { "site": …, "goals": […] } — an object, not a bare array — listing every goal name ever recorded for the site; from/to are ignored on that branch, so it spans all time. Add goal=<name> with a from/towindow to get that goal’s conversions over time and its conversion rate by entry page; the optional bucket is one of hour, day (default), week or month. conversions_by_landing covers every landing page active in the window, not only the ones that converted, and is uncapped — landing_path is the raw first-touch path, query string included. (Recording a conversion is a write: POST /api/v1/goals.)
# List every goal recorded for the site
curl "https://app.statlark.com/api/v1/goals?site=sl_xxxx" \
-H "Authorization: Bearer slk_your_token_here"
# One goal's conversions over time + a rate for every entry page
curl "https://app.statlark.com/api/v1/goals?site=sl_xxxx&goal=signup&from=2026-06-01&to=2026-06-04&bucket=day" \
-H "Authorization: Bearer slk_your_token_here"{
"site": "sl_xxxx",
"goal": "signup",
"range": { "from": "2026-06-01T00:00:00.000Z", "to": "2026-06-04T00:00:00.000Z" },
"bucket": "day",
"timeseries": [
{ "bucket_start": "2026-06-01T00:00:00+00:00", "conversions": 18 },
{ "bucket_start": "2026-06-02T00:00:00+00:00", "conversions": 9 },
{ "bucket_start": "2026-06-03T00:00:00+00:00", "conversions": 13 }
],
"conversions_by_landing": [
{ "landing_path": "/pricing", "landing_visitors": 900, "conversions": 34, "conversion_rate": 0.03777777777777778 },
{ "landing_path": "/?utm_source=toolify", "landing_visitors": 240, "conversions": 6, "conversion_rate": 0.025 },
{ "landing_path": "/blog/revenue-attribution", "landing_visitors": 612, "conversions": 0, "conversion_rate": 0 }
]
}Realtime
GET /api/v1/realtime — the right-now snapshot: active visitors (distinct in the last 5 minutes) and the most recent events (last 30 minutes, newest first, capped at 50— there’s no pagination and no truncation flag, so a busy site’s feed is incomplete). No from/to— it’s always “now”.
curl "https://app.statlark.com/api/v1/realtime?site=sl_xxxx" \
-H "Authorization: Bearer slk_your_token_here"{
"site": "sl_xxxx",
"active_visitors": 7,
"recent_events": [
{
"occurred_at": "2026-07-01T12:04:11.812431+00:00",
"type": "pageview",
"name": null,
"path": "/pricing",
"country": "US",
"region": "CA",
"city": "San Francisco",
"channel": "Organic",
"visitor_id": "b1e8f0a2-4c77-4b3e-9a51-2f0d6c8ea310"
}
]
}Site metadata
GET /api/v1/site — the config the other endpoints report against: domain, timezone, currency and the #1 KPI goal. No from/to.
curl "https://app.statlark.com/api/v1/site?site=sl_xxxx" \
-H "Authorization: Bearer slk_your_token_here"{
"site": "sl_xxxx",
"domain": "example.com",
"timezone": "America/New_York",
"currency": "USD",
"primary_kpi_goal": "signup"
}Writing data server-side
Send conversions, revenue and identity straight from your own server, so they survive ad blockers and become the source of truth. These endpoints need a token with the ingest:write permission; each is a POST (or DELETE) with a JSON body, and always writes to the token’s own site. Attribute a hit to a visitor by including the visitor_id you captured client-side with statlark.getVisitorId().
Payments
POST /api/v1/payments — record revenue from any provider (Stripe, LemonSqueezy, Polar, Paddle…). Idempotent on transaction_id, so retries are safe: created is true on the insert and false for a duplicate. amount is in major units (e.g. 49.00, not 4900).
Also accepted: customer_email (stitches the sale to a person), session_id, provider (defaults to api), occurred_at (an ISO date or timestamp; defaults to now) and raw— your provider’s original payload, which must be a JSON object and at most 16,384 bytes. currency defaults to USDand must be a three-letter code. Send one that isn’t the site’s configured currency and the call still returns created: true, but the payment is excluded from every revenue figure rather than converted.
curl -X POST "https://app.statlark.com/api/v1/payments" \
-H "Authorization: Bearer slk_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"transaction_id": "ch_3Q7...",
"amount": 49.00,
"currency": "USD",
"visitor_id": "b1e8f0a2-...",
"customer_email": "jo@example.com"
}'{ "ok": true, "created": true }Refunds
To net a refund out of your revenue, POST to the same /api/v1/payments endpoint with a refunded_amount instead of an amount. It’s matched to the original sale by transaction_idand subtracted from revenue everywhere — Overview, sources, pages and each customer’s lifetime value. refunded_amount is the cumulativetotal ever refunded on that transaction (in major units), so it’s safe to send the same value twice — the call is idempotent and never lowers a larger refund already recorded. A refund is credited against the original sale’s date, so it appears in any report window that contains the purchase. Optionally send a status (refunded, partially_refunded or disputed); omit it and Statlark derives it from the amounts. If no payment carries that transaction_id, you get a 404.
curl -X POST "https://app.statlark.com/api/v1/payments" \
-H "Authorization: Bearer slk_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"transaction_id": "ch_3Q7...",
"refunded_amount": 49.00
}'{
"ok": true,
"matched": true,
"transaction_id": "ch_3Q7...",
"amount": 49,
"refunded_amount": 49,
"net_amount": 0,
"status": "refunded"
}Deleting a payment
DELETE /api/v1/payments — erase one recorded payment by transaction_idand recompute the affected customer’s lifetime value. Use it to correct a mis-recorded sale, or to honor a data deletion request. Returns 404 if nothing matches. (To net a refund, prefer the refund call above — DELETE removes the sale entirely, as if it never happened.)
curl -X DELETE "https://app.statlark.com/api/v1/payments" \
-H "Authorization: Bearer slk_your_token_here" \
-H "Content-Type: application/json" \
-d '{ "transaction_id": "ch_3Q7..." }'{ "ok": true, "deleted": true }Goals
POST /api/v1/goals — the server-side twin of statlark.goal(). Needs the visitor_id and a name; optional props are capped like everywhere else (≤20 keys, primitive values). Unlike payments this write is not idempotent— there’s no dedupe key, so a retried webhook records a second conversion — and it can’t be backdated: the conversion is timestamped when it arrives.
curl -X POST "https://app.statlark.com/api/v1/goals" \
-H "Authorization: Bearer slk_your_token_here" \
-H "Content-Type: application/json" \
-d '{ "visitor_id": "b1e8f0a2-...", "name": "signup", "props": { "plan": "pro" } }'{ "ok": true }Identify
POST /api/v1/identify — attach a user_id, user_email and traitsto a visitor for cross-device, revenue-tied identity. Only the fields you send are updated, so a call carrying just new traits won’t clear an existing email — but a field sent explicitly as null, "" or any non-string does clear it. traits replaces the stored traits wholesale rather than merging, and is capped the same way as goal props.
curl -X POST "https://app.statlark.com/api/v1/identify" \
-H "Authorization: Bearer slk_your_token_here" \
-H "Content-Type: application/json" \
-d '{
"visitor_id": "b1e8f0a2-...",
"user_id": "user_123",
"user_email": "jo@example.com",
"traits": { "plan": "pro" }
}'{ "ok": true }Rate limits
Two limits protect the API. Each request is capped at 600 per minute per IP address and, once authenticated, 300 per minute per token. Both are token buckets: you can burst up to the limit, then draw down at the sustained rate (10 and 5 requests per second respectively). These ceilings are generous for normal use — polling, dashboards, server-side ingestion — and exist to absorb runaway loops and abuse.
Every 200 and every 429 carries your current quota — X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the bucket refills) — so you can slow down before you hit the ceiling rather than after. The other error statuses (400, 401, 403, 404, 500) carry none of the three. And when the per-IP limit is what tripped, the headers describe that IP bucket (600), not your per-token quota.
Exceed either limit and the request returns 429 with those same headers plus Retry-After (seconds to wait). Back off and retry after Retry-After; if you’re hitting these regularly, batch your calls or get in touchand we’ll raise them.
HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 60
{ "error": "Rate limit exceeded — slow down and retry shortly." }Errors
Errors come back with the matching HTTP status and a JSON body of the shape below. 400 means a parameter or body field is missing or invalid; 401 means the token is missing, malformed or revoked; 403means the token lacks the permission the call needs, or can’t read the requested site; 404 means the record you addressed is gone — no payment carries that transaction_id, or the site row was deleted mid-request; 429means you’ve hit a rate limit (see above); 500 means the query or the token lookup failed on our side, and retrying a read is safe.
{ "error": "This token can't read that site." }Amounts are in the site’s configured currency, as major units (e.g. dollars, not cents) — a payment recorded in any other currency is left out of revenue entirely, never converted. Every response is JSON and served no-store — these are live numbers, never cached at the edge.
Last reviewed