Authentication

Bearer API tokens, the entity-plus-action scope model, and why default-deny means an unscoped token is refused rather than allowed.

Every public API request carries a bearer token:

GET /v1/tables HTTP/1.1
Host: api.foxguide.io
Authorization: Bearer fgd_live_0123456789abcdef...

Tokens are issued in the platform UI with a user session, not through this API. Token management lives at /v1/api-tokens, which is deliberately not part of the public surface — you cannot mint a token with a token. The secret is shown once, at creation; the platform stores only its hash and can never show it to you again.

Scopes are an entity crossed with an action

A token carries a list of scopes. Each scope names one entity and the actions allowed on it, drawn from read, write and delete:

{
  "scopes": [
    { "entity": "table",  "actions": ["read", "write"] },
    { "entity": "export", "actions": ["read"] }
  ]
}

The entity list is part of the contract and grows over time; the reference below is the authority for which entity a given endpoint requires. Do not hard-code a list from prose — including this page.

Default-deny, and what that actually means

Authorisation is fail-closed. A request is allowed only when the token holds a scope naming the endpoint’s entity and that scope’s action list contains the action the endpoint performs. Everything else is refused.

Two cases are worth stating plainly, because they are the ones that surprise integrators:

  • A token carrying no scope list at all is refused, not allowed. An absent list is treated exactly like an empty one.
  • A token holding read on an entity is refused on that entity’s write endpoints. Actions are not hierarchical and write does not imply delete.

INSUFFICIENT_SCOPE — the refusal you will actually meet

When a token is authenticated but not scoped for what it asked for, the API answers 403 with the flat error envelope and the code INSUFFICIENT_SCOPE:

HTTP/1.1 403 Forbidden
Content-Type: application/json

{
  "error": "INSUFFICIENT_SCOPE",
  "message": "API token does not have scope export:write"
}

The message names both terms of the refusal — the entity and the action — so the fix is mechanical: re-issue the token with that entity and action in its scope list.

INSUFFICIENT_SCOPE is the public API’s primary refusal. It is what a correctly-authenticated caller sees whenever the request falls outside what its token was granted, and under default-deny that is every request the token was not explicitly issued for.

Distinguish it from its neighbours:

statuscodewhat happened
401UNAUTHORIZEDthe token is missing, malformed, expired or revoked — you are not authenticated at all
403INSUFFICIENT_SCOPEyou are authenticated; this token does not hold the scope this endpoint requires
404<DOMAIN>_NOT_FOUNDauthenticated and scoped; the resource does not exist

A 403 is never a reason to retry. Retrying with the same token produces the same refusal, because nothing about the request changed — re-issue the token instead.

See also

  • Getting started — issuing a token, the once-only secret, and rotating with a 24-hour grace period
  • Errors — the flat envelope and which refusals are worth retrying
  • Rate limits — the other 4xx a working integration meets, and the one 403 (IP_NOT_ALLOWED) that is not about scopes
  • API reference — the authority for which entity and action a given endpoint requires

← All documentation