Sign in
Colour theme

Analytiics HTTP API reference

Two hosts. The collector at in.analytiics.co takes events; the app at www.analytiics.co manages projects and serves dashboards.

All request and response bodies are JSON.

The machine-readable version

Everything on this page is also published as an OpenAPI 3.1 document, generated from the same source rather than transcribed from this prose:

Every operation there carries a unique operationId, a description of when to call it and what it will not do, typed parameters, and a response schema for every status — including the failures. It is meant to be usable as-is for generating a client or a set of tool definitions.

Read the spec when you are writing code against this API. Read the rest of this page when you want to know *why* an endpoint behaves the way it does.

Authentication

There are two credentials, and they are not interchangeable.

  • A server write key (sk_live_…), sent as Authorization: Bearer <key>, authorizes writes to exactly one project. It is shown once, when issued, and only its hash is stored. Keep it server-side.
  • A session token, also sent as Authorization: Bearer <token>, is what analytiics login obtains through the device flow. It authorizes the management endpoints on the app host as the person who signed in. Browsers use the same session as a cookie.

The public collector needs no credential at all, which is why it refuses to accept revenue.

POST /v1/events

Public, CORS-enabled, unauthenticated. What the browser tracker calls.

POST https://in.analytiics.co/v1/events
Content-Type: text/plain
{
  "events": [
    {
      "site": "example.com",
      "name": "page_published",
      "url": "https://example.com/editor",
      "ref": "https://www.google.com/",
      "sid": "…",
      "uid": "",
      "props": { "template": "grid" }
    }
  ]
}

Up to 50 events per request. site and name are required; ts is optional and defaults to arrival. Location and device are derived from the request headers, so nothing about the visitor's IP is stored.

Responses: 202 {"accepted": n} on success — including for traffic identified as a bot, which is silently dropped with accepted: 0400 {"error": "invalid payload"} for anything that fails validation, and 404 with code project_not_found when site names a project that does not exist, and 403 with code site_mismatch when the page is not on that project's domain or a subdomain of it. Events are only recorded for a project somebody has created, from pages on its domain, so a snippet installed under the wrong domain — or on the wrong site — fails visibly in the network tab rather than filling a dashboard nothing can claim. Pages on localhost are let through for local development. If the collector cannot check — its control plane is unreachable and it has no recent answer for that site — it answers 503 and records nothing rather than guess.

This endpoint ignores revenue. It is reachable by anyone who can read your page source, so accepting money over it would let a stranger fill your dashboard with fake sales.

POST /v1/server/events

Authenticated, server-to-server, no CORS. This is the only path that may set revenue; browser identify calls also carry identity.

POST https://in.analytiics.co/v1/server/events
Authorization: Bearer sk_live_…
Content-Type: application/json
{
  "site": "example.com",
  "events": [
    {
      "event_id": "evt_provider_123",
      "name": "subscription_started",
      "user_id": "usr_123",
      "revenue_cents": 2900,
      "currency": "USD",
      "props": { "plan": "pro" }
    }
  ]
}

Up to 200 events per request. Every event requires a stable event_id; reuse it for retries of the same logical event. The Node SDK generates IDs by default, and webhook callers should supply their provider event ID. revenue_cents is a signed integer, so refunds and chargebacks are ordinary events; currency is a three-letter code and is required whenever revenue_cents is set — an amount with no unit cannot be summed, and defaulting to USD would silently misreport every other customer.

Responses: 202 {"accepted": n}, 400 with a detail naming the first validation failure, 401 for a missing, wrong or unknown key — identical whether the project exists or not, so the endpoint cannot be used to enumerate projects. 409 reports conflicting reuse of an event ID. 503 with Retry-After means authorization or durable delivery could not be confirmed. Retry the same IDs with backoff; a partial write may have committed. 202 confirms storage of every logical event, including previous deliveries.

GET /health

200 {"ok": true}. No authentication.

GET /script.js

The browser tracker, cached and CORS-open. Under 2 KB gzipped. Named for what it is rather than for us: filter lists carry path rules that apply on any host, so a vendor-shaped filename would follow you onto your own collector domain. Also served at /aii.js, the name it had first.

Management endpoints

On the app host, authenticated with a session token or cookie.

  • GET /api/projects — the projects you own.
  • POST /api/projects — create one, from its domain. A project is its domain: it is what the tracker sends, where the dashboard lives, and the id on every stored event, so it is chosen once and there is no rename.
  • GET /api/projects/{slug} — one project: domain, timezone, age.
  • DELETE /api/projects/{slug} — delete it. Keys, settings and share links go with it; events already recorded stay in the analytics store, and the domain stays permanently reserved. Retained analytics cannot transfer to a new project.
  • GET /api/projects/{slug}/keys — list a project's write keys, by prefix and creation date. Key material is never returned.
  • POST /api/projects/{slug}/keys — issue one. The response is the only time the key is readable.
  • DELETE /api/projects/{slug}/keys/{id} — revoke one. A tombstone, not a delete: the key stays listed with the date it died.
  • GET /api/projects/{slug}/dashboard — the dashboard's publication settings.
  • PATCH /api/projects/{slug}/dashboard — change what an anonymous reader sees, widget by widget, and set the project's timezone and the range its dashboard opens on.
  • GET /api/usage — your plan, and what this month has used of it.
  • POST /api/checkout — start a checkout for a paid plan.
  • POST /api/billing/portal — invoices, the card on file, and cancelling.

GET /api/usage

GET https://www.analytiics.co/api/usage

Your plan and this billing period's usage. Account-wide, because a plan is: the cap is bought once and shared by every project you own.

{
  "plan": { "id": "free", "name": "Free", "priceCents": 0, "events": 25000, "projects": 1 },
  "period": { "startsAt": "2026-08-01T00:00:00.000Z", "endsAt": "2026-09-01T00:00:00.000Z" },
  "events": 18402,
  "level": "approaching",
  "measured": true,
  "sampled": false,
  "projects": [{ "slug": "thiings", "events": 18402 }],
  "projectCount": 1
}

Three things it is worth knowing about that number:

  • What counts. A page view, and the events you name yourself. The tracker's own $outbound and $identify calls do not — they are sent because the snippet is installed rather than because you asked for them.
  • Which month. The calendar month in UTC, not each project's timezone. A dashboard's "today" belongs to its project; a bill has one clock, or an account with projects in Auckland and Los Angeles would be billed for overlapping months.
  • `measured: false` is not zero. It means the analytics store could not be counted, and events is then null. Nothing has been capped — the meter is what is missing, not the events.

level is ok, then approaching from 80% of the cap, then over from 100%. None of them stops ingestion: caps here are soft, and nothing is cut mid-period. What happens instead is sampling. An hourly sweep meters every account; past 100% it marks the account, and from then until the month resets the collector keeps one browser event in ten — whole sessions at a time, so a funnel drawn from a sampled month is still a funnel. Server events (revenue, identity) are never sampled. sampled says whether that is happening right now; it clears when the month resets or the plan changes, including the moment a checkout completes. The sweep emails once at each of the two levels per month, with the next plan as an offer.

What the cap does refuse is a *new project* past the plan's project ceiling — POST /api/projects answers 402 with code: "plan_limit", and names the plan that would cover it. Creation is also rate limited to twenty an hour per account (429, with a Retry-After), whatever the plan.

Paying, from a terminal

The whole upgrade is one command, and it is worth saying exactly what each half of it does:

analytiics upgrade maker

It asks POST /api/checkout for a URL, opens it, and then polls GET /api/usage until the plan actually changes.

{ "url": "https://checkout.stripe.com/c/pay/cs_test_…", "plan": "maker" }

Creating a checkout grants nothing. All that endpoint can do is mint a link to a page hosted by Stripe, where a person types a card number. The plan moves afterwards, when Stripe tells our server that money moved — so there is no request a client can make that upgrades an account, and nothing for an agent to get wrong. Card details never reach us; the checkout page is the human's confirmation step, and it is structurally the only one there is.

That is also why the CLI polls rather than reporting success: it cannot know whether anybody paid, and the server can. If you are building your own flow, do the same — treat a 201 here as "a link exists", and getUsage's plan as the truth.

POST /api/billing/portal returns a link to Stripe's billing portal, where the same person can read invoices, change the card, or cancel. The customer is taken from your session, so there is no way to point it at somebody else's billing. 404 no_subscription means the account has never been billed and there is nothing to show.

Nothing else moves a plan. The webhook Stripe calls is not listed above and is not part of this API: it is authenticated by a signature over the exact bytes Stripe sent, not by a session, and it is the only writer of the plan on an account.

GET /api/online

GET https://www.analytiics.co/api/online?project=example.com

The lightweight endpoint for live stats pills. Returns only {"online": 42}, counting people seen within the last five minutes, with 30-second shared caching. It does not fetch the activity feed. Poll every 30 seconds while visible; pause in background tabs.

The project must publish its dashboard and enable realtime. Publication is checked on every request, including requests from the owner. Responses are cross-origin readable and use Cache-Control: private, no-store. Private or realtime-disabled projects return 404; rate limits return 429 with Retry-After. Hide the pill if the request fails.

For a complete embeddable component, see the live stats pill guide.

GET /api/realtime

GET https://www.analytiics.co/api/realtime?project=example.com

Cross-origin readable on purpose: it returns exactly what an anonymous visitor already sees on the public dashboard: the live count and activity feed. For a count-only pill, use /api/online. Served for projects that publish it, with 30-second shared caching, under per-project and per-client rate limits; past those it answers 429 with a Retry-After.

For a complete embeddable component, see the live stats pill guide.

GET /api/live

GET https://www.analytiics.co/api/live?project=example.com

What the live row on a dashboard shows: the count, the recent events, and the coordinates the globe plots. Gated widget by widget — a project that publishes the map but not the stream gets an empty feed here, not the data behind it.

Unlike /api/realtime this sends no CORS headers. A live count is something a site displays on its own pages; a list of who is currently on it is not.

POST /api/waitlist

Public. {"email": "you@example.com", "source": "hero"}, answering 200 {"ok": true}. The same answer whether the address was new or already on the list — "you're already on it" would make this a way to ask whether a given person signed up.

Errors

Every failure on both hosts has the same JSON body:

{
  "error": "no such project",
  "code": "project_not_found",
  "message": "no such project",
  "hint": "Check the domain. This is also the answer for a project that exists but belongs to someone else, so it never confirms whether a domain is taken.",
  "docs": "https://www.analytiics.co/docs/api"
}

Switch on code. It is stable, it maps to exactly one HTTP status, and unlike the sentence it will not be reworded. error and message carry the same sentence: error is the field existing Analytiics clients already read, message the one a generic client looks for. A validation failure adds detail, naming the first field that failed. The full list of codes is the code enum in the OpenAPI document.

Three of them are worth knowing before you meet them:

  • rate_limited (429) always comes with Retry-After. Wait it out and keep showing the last good response rather than clearing it.
  • key_verification_unavailable (503) means your write key may well be valid and we could not check it. It is deliberately not a 4xx: retry rather than dropping the event.
  • unauthorized (401) on the server event path is the same answer whether the key is wrong or the project does not exist, so it cannot be used to enumerate projects.

A path that does not exist answers 404 with code: "not_found" — as JSON on both hosts, never as an HTML error page. A path that exists but does not accept the method you used answers 405 with code: "method_not_allowed" and an Allow header listing the methods it does take.

Rate limits and ceilings

Public dashboard rendering, filtered rendering and realtime polling each have a per-project ceiling per minute. Past a ceiling, the last good render is served rather than a fresh query — a page going viral costs a cached response rather than a bill.

Last updated 2026-08-27. This page is also available as Markdown at /docs/api.md, or from this URL with Accept: text/markdown.