Docs · Reference

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 returns 403.
  • from and to — 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)from is included, to is 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