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):
- In the dashboard, open Organization settings → API keys and choose Create key.
- 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.
- 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:writeandrevenue:write. Anything else you ask for is left out without an error, so check thepermissionsof the response: it is what the key got. If none of the requested permissions can be granted, the request fails with400. - 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:
| Endpoint | Permission | What it does |
|---|---|---|
POST /api/v1/orgs/{orgId}/api-keys | apiKey:create | Creates a key and returns it, the only time it is returned. |
GET /api/v1/orgs/{orgId}/api-keys | apiKey:read | Lists 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:delete | Deletes (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:
{
"name": "Reporting script",
"permissions": { "analytics": ["read"] },
"siteIds": ["SITE_ID"],
"expiresInDays": 90
}| Field | Required | Rules |
|---|---|---|
name | yes | 1 to 64 characters. |
permissions | yes | An object of resource → actions, for example { "analytics": ["read"], "goals": ["read", "write"] }. An unknown resource or action is a 400. See Permissions. |
siteIds | no | 1 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. |
expiresInDays | no | 1 to 3650. Omit it for a key that never expires. |
{
"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:
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: Beareris read as an API key only when the token starts withak_(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
| Answer | Why |
|---|---|
401 Invalid API key | The key is unknown, expired or deleted. |
401 Authentication required | No credential, on an endpoint that needs one. |
404 Site not found | The 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 found | The {orgId} is not the key's organization. |
403 Forbidden | The 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 requests | The 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:
| Credential | Sent as | Used by |
|---|---|---|
| Session cookie | Cookie (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 token | x-share-token header, or ?share= | Share links: read-only access to one site, with the scopes the link was made with. |
| MCP OAuth token | Authorization: Bearer (or DPoP with a DPoP proof) at /api/mcp only | AI assistants signed in with a PilotFox account through the MCP server. These tokens are not accepted on /api/v1. |
| Affiliate portal token | x-affiliate-token header, at GET /api/v1/affiliate-portal only | An 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-keyandAuthorization: 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.