PilotFoxDocs

REST API overview

The PilotFox REST API: base URL, request and response format, ids, and the OpenAPI document behind the reference.

Everything the PilotFox dashboard shows comes from a REST API, and the same API is open to your own code: read your analytics into a report or a spreadsheet, manage goals and funnels, send events from your server, or record payments from a billing system PilotFox does not connect to.

Base URL

https://pilotfox.app/api/v1

A self-hosted instance answers at its own address: PUBLIC_URL followed by /api/v1 (for example https://analytics.example.com/api/v1). Every example in these docs uses https://pilotfox.app; replace it with your instance's URL if you run your own.

A first request

Create an API key. In the dashboard, open Organization settings → API keys and create a key with the analytics:read permission. The key is shown once; keep it on your server. See Authentication with API keys.

Find your site id. It is the value of data-site in your tracking script tag, and the id of each site that GET /api/v1/orgs/{orgId}/sites lists.

Ask for the last seven days.

curl -H "Authorization: Bearer $PILOTFOX_API_KEY" \
  "https://pilotfox.app/api/v1/sites/SITE_ID/overview?last=7d"
Response
{
  "data": {
    "visitors": 8,
    "sessions": 8,
    "pageviews": 7,
    "events": 2,
    "bounce_rate": 85.71428571428571,
    "avg_session_duration": 0.009285714285714286,
    "median_session_duration": 0,
    "views_per_session": 1
  },
  "meta": {
    "window": {
      "from": "2026-10-20T20:29:45.263Z",
      "to": "2026-10-27T20:29:45.263Z",
      "tz": "Europe/Berlin",
      "bucket": "day"
    },
    "cache": "miss"
  }
}

Requests

  • JSON in, JSON out. Send request bodies as application/json. A JSON body may be up to 1 MiB; a larger one is answered 413 (payload_too_large). File uploads to imports are multipart/form-data with a limit of their own.
  • Query parameters carry the time window, filters and paging of the analytics endpoints. Filters are a JSON array in one parameter, so URL-encode it (curl's -G --data-urlencode does). See Time windows, filters and comparisons.
  • One credential per request: an API key, or (for the dashboard) a session cookie, or a share token. Sending two is a 400. See Authentication.
  • From your server only. The API answers cross-origin browser requests only from the instance's own origin (and any DASHBOARD_ORIGINS a self-hosted instance configures), so a web page on another site cannot call it, and an API key in a web page would be readable by anyone. Browsers send data to PilotFox through the tracking script, which needs no key.

Responses

A successful response is an object with data and, on the analytics endpoints, meta:

{ "data": { "visitors": 8 }, "meta": { "window": { "from": "…", "to": "…", "tz": "Europe/Berlin", "bucket": "day" } } }
meta fieldWhenWhat it holds
windowanalytics endpointsThe resolved window: from and to (ISO 8601 instants, UTC; from is null for all=true), the time zone tz and the bucket of time series.
comparewhen you asked for a comparisonfrom and to of the comparison window.
totalCountreports paged by limit and offsetHow many rows exist across all pages.
nextCursorlists paged by cursorThe cursor of the next page, or null on the last one.
truncatedmap points, the session map timeline, live visitorstrue when there were more rows than the answer holds.
cachecached reportshit, miss or coalesced (another identical request was already running and its result was shared).

Other endpoints (sites, API keys, goals, segments and the like) answer { "data": … } alone. Creating something answers 201, starting a background job 202, and some deletes and writes 204 with no body; each reference page lists the statuses of its endpoint.

An error is an object with error, and the HTTP status matches it:

{ "error": { "code": "not_found", "message": "Site not found" } }

The Errors page lists every code.

Field names, ids and times

  • Analytics rows (overview, breakdowns, sessions, events, payments) use snake_case fields: bounce_rate, session_id, visitor_id. Stored resources and their request bodies (sites, API keys, goals, funnels, segments, annotations, share links) use camelCase: siteIds, expiresAt, isPublic. The bodies that record data, server-side ingest and the payments API, use snake_case like the rows they write: anonymous_id, payment_id.
  • 64-bit ids are strings, for example "session_id": "10753433134694004327", because JSON numbers cannot hold them exactly. Keep them as strings in your code.
  • Times are ISO 8601 strings in UTC (2026-10-27T20:29:45.263Z).
  • Sites are addressed by their public id (a short string such as 74lku1zte8m8fpyg): the {siteId} of every /api/v1/sites/{siteId}/… path, the data-site of the script tag and the site of an ingested event.
  • Organizations are addressed by their id, a UUID: the {orgId} of every /api/v1/orgs/{orgId}/… path. It appears in the dashboard's address bar on organization pages (/orgs/{orgId}/settings).

Cached reports

Analytics reports are cached for a short time (30 seconds by default; QUERY_CACHE_TTL_SECONDS on a self-hosted instance), shared by every caller asking the same question, and meta.cache says whether you got a cached result. A caller holding site:read on the site (organization members, and API keys given that permission) can send Cache-Control: no-cache to skip the cache; from any other caller the header is ignored. Lists of sessions, users, raw events and payments, and the realtime endpoints, are never cached.

The OpenAPI document

Every instance describes its own API:

URLWhat
https://pilotfox.app/api/docs/The interactive API reference, served by the instance.
https://pilotfox.app/api/docs/openapi.jsonThe OpenAPI 3.1 document behind it, generated from the API's own request and response schemas (openapi.yaml too).

The API reference in these docs is generated from the same document: one page per endpoint, with its parameters, request body and responses. Feed the document to a code generator to get a typed client.

In this section

On this page