PilotFoxDocs

Authentication with API keys

Create organization API keys, send them with each request, limit them to sites and permissions, and revoke them.

Integrations authenticate with organization API keys. A key acts for its organization, with the permissions chosen when it was created, on every site of the organization or only on some.

Create a key

Organization owners and admins create keys (they hold apiKey:create; no other role does):

  1. In the dashboard, open Organization settings → API keys and choose Create key.
  2. Give it a name (up to 64 characters), tick the permissions it needs, choose All sites or Only selected sites, and an expiry: 7 days, 30 days, 90 days, 1 year or never.
  3. Copy the key. It is shown once. PilotFox stores only a hash of it and cannot show it again; if you lose it, create another and delete the old one.

A key looks like ak_ followed by 64 letters. The list shows its first six characters (start, such as ak_Xpx) so you can tell keys apart.

What a key can be given

  • No more than your own role. The permissions a key receives are the ones you asked for that your own role holds, plus, for owners and admins, the two key-only permissions ingest:write and revenue:write. Anything else you ask for is left out without an error, so check the permissions of the response: it is what the key got. If none of the requested permissions can be granted, the request fails with 400.
  • Sites. Without a site list the key works on every site of the organization, now and later. With one, it works only on those sites (up to 500, by their public id), and each must be a site of the organization you can see. See Site restrictions for what a restricted key can and cannot reach.
  • Expiry. From 1 to 3650 days, or none. An expired key is refused like an unknown one.
  • Fixed afterwards. A key's permissions, sites and expiry cannot be edited. To change them, create a new key and delete the old one.

A key belongs to the organization, not to the person who created it: its permissions stay what they were at creation, whatever happens to that person's role later. Deleting the organization deletes its keys.

The per-organization cap

An organization holds at most 50 active API keys (unexpired; API_KEY_MAX_PER_ORG on a self-hosted instance). Creating one more is refused with 409 (conflict): This organization has its maximum of 50 active API keys: delete one first. Each key has its own request budget, so the cap also bounds what one organization's keys can send.

Through the API

The dashboard manages keys through three endpoints:

EndpointPermissionWhat it does
POST /api/v1/orgs/{orgId}/api-keysapiKey:createCreates a key and returns it, the only time it is returned.
GET /api/v1/orgs/{orgId}/api-keysapiKey:readLists the organization's keys: id, name, start, permissions, siteIds (null for every site), createdAt, lastUsedAt, expiresAt. Never the key itself.
DELETE /api/v1/orgs/{orgId}/api-keys/{keyId}apiKey:deleteDeletes (revokes) a key: { "data": { "deleted": true } }.

Only a signed-in person can create a key: an API key calling the create endpoint gets 403, whatever it holds (keys never mint keys). Listing and deleting also work with an API key that holds apiKey:read or apiKey:delete and has no site list. The body of a creation:

POST /api/v1/orgs/ORG_ID/api-keys
{
  "name": "Reporting script",
  "permissions": { "analytics": ["read"] },
  "siteIds": ["SITE_ID"],
  "expiresInDays": 90
}
FieldRequiredRules
nameyes1 to 64 characters.
permissionsyesAn object of resource → actions, for example { "analytics": ["read"], "goals": ["read", "write"] }. An unknown resource or action is a 400. See Permissions.
siteIdsno1 to 500 site public ids. Omit it for every site; only someone who sees every site of the organization (owners and admins always do) can create such a key.
expiresInDaysno1 to 3650. Omit it for a key that never expires.
Response (201)
{
  "data": {
    "id": "759e8d8d-cfc9-4743-beab-d9564eb0573b",
    "key": "ak_…",
    "start": "ak_Xpx",
    "permissions": { "analytics": ["read"] },
    "siteIds": ["SITE_ID"],
    "expiresAt": "2027-01-25T20:21:12.046Z"
  }
}

Send the key

Send the key with every request, in either header:

curl -H "Authorization: Bearer $PILOTFOX_API_KEY" \
  "https://pilotfox.app/api/v1/sites/SITE_ID/overview?last=7d"
Node.js (18 or later)
const res = await fetch("https://pilotfox.app/api/v1/sites/SITE_ID/overview?last=7d", {
  headers: { Authorization: `Bearer ${process.env.PILOTFOX_API_KEY}` },
});
if (!res.ok) throw new Error(`PilotFox ${res.status}: ${(await res.json()).error.message}`);
const { data, meta } = await res.json();
  • Authorization: Bearer is read as an API key only when the token starts with ak_ (the scheme is case-insensitive). Any other bearer token is not a PilotFox credential on /api/v1, and the request counts as unauthenticated.
  • Sending the same key in both headers is fine; two different keys are refused (see below).

When a key is refused

AnswerWhy
401 Invalid API keyThe key is unknown, expired or deleted.
401 Authentication requiredNo credential, on an endpoint that needs one.
404 Site not foundThe site is not one the key can reach (another organization's, outside its site list, deleted or unknown). The analytics read endpoints, which also serve public sites, answer this for an unauthenticated request to a private site.
404 Organization not foundThe {orgId} is not the key's organization.
403 ForbiddenThe key reaches the site or organization but lacks the permission, or it is limited to some sites and the endpoint is an organization one that needs every site (see Site restrictions).
429 Too many requestsThe key used up its request budget, or this client sent too many refused credentials.

PilotFox never says whether a site exists to a caller who cannot see it: an unknown site and another organization's site get the same 404.

Revoke a key

Organization settings → API keys → Revoke, or the DELETE endpoint above. From then on, requests with the key are 401. PilotFox caches a key's verification for up to 60 seconds and clears that cache when the key is deleted; only a request being verified at that very moment can leave the deleted key working for up to a minute. For the same reason lastUsedAt can be up to a minute behind.

Keep keys on your server

An API key is a password for your organization's data. Keep it in a server-side secret (an environment variable, a secret manager), never in a web page, a mobile app or a public repository: anything shipped to a browser or a device can be read. The API does not answer cross-origin browser requests from other sites anyway.

To collect data from browsers, use the tracking script, which needs no key. To send events from your backend, use a key with ingest:write and the server-side ingest endpoints.

Give each integration its own key with the fewest permissions and sites it needs, and an expiry where you can, so a leaked key exposes little and can be revoked without breaking the others.

Other credentials

The API accepts four other credentials. Integrations rarely need them, but they explain some answers:

CredentialSent asUsed by
Session cookieCookie (set by signing in at /api/auth)The dashboard. A cookie-authenticated POST, PUT, PATCH or DELETE must come from the instance's own origin (Origin header) with a JSON or multipart body, or it is refused (403, or 415 for another content type). API keys and share tokens are exempt from that check.
Share tokenx-share-token header, or ?share=Share links: read-only access to one site, with the scopes the link was made with.
MCP OAuth tokenAuthorization: Bearer (or DPoP with a DPoP proof) at /api/mcp onlyAI assistants signed in with a PilotFox account through the MCP server. These tokens are not accepted on /api/v1.
Affiliate portal tokenx-affiliate-token header, at GET /api/v1/affiliate-portal onlyAn affiliate reading their own report through the affiliate portal.

A site made public in its settings opens its analytics read endpoints to everyone, without any credential.

One credential per request

A request carries one credential. These combinations are refused with 400, before the API key or share token is verified:

  • an API key together with a share token;
  • an API key or a share token together with a cookie of a valid dashboard session (a stale cookie does not count);
  • two different API keys (x-api-key and Authorization: Bearer), or two different share tokens.
{
  "error": {
    "code": "bad_request",
    "message": "Send exactly one credential",
    "details": { "reason": "ambiguous_credentials" }
  }
}

If you call the API from a tool that keeps your dashboard cookies (a browser extension, an HTTP client sharing a cookie jar), clear them or use a separate client for API-key requests.

On this page