API authentication
The Fanzava API authenticates machine clients with a hub API key. You issue the key in your hub, your client sends it on every request, and the scopes on the key decide what it can reach.
There is no OAuth flow. Fanzava runs no authorization server, so there is nothing to fetch at /.well-known/openid-configuration or /.well-known/oauth-authorization-server, and no authorization code, refresh token or consent screen anywhere in the process.
Base URL
Section titled “Base URL”Every request goes to https://api.fanzava.com.
Every route that acts on a hub carries the hub id in the path:
https://api.fanzava.com/api/v1/hubs/{hubId}/...There is no ambient tenant. A hub-scoped route called without the hub id in the path is refused with HTTP 400 and the error code HUB_CONTEXT_MISSING, even when your key belongs to exactly one hub. Naming the hub in a header or a query parameter does not work and is not a supported alternative.
Issue a key
Section titled “Issue a key”A hub admin creates keys in the hub admin console, under Settings, Integrations, API keys. When you create a key you choose:
- A name. How the key is identified in the list and in the audit trail.
- Its scopes. Tick only what the integration needs. A key holds no role and inherits nothing from the admin who created it, so its scopes are the whole of its authority.
- An optional expiry. A key with an expiry stops working on that date with no further action.
Revoking a key in the same screen kills it immediately.
Present the key
Section titled “Present the key”A key looks like fz_live_<id>:<secret>. Send the whole string, prefix and colon included, in either header:
GET /api/v1/hubs/{hubId}/leaderboards HTTP/1.1Host: api.fanzava.comAuthorization: Bearer fz_live_a1b2c3d4e5f6g7h8:AbCdEf...GET /api/v1/hubs/{hubId}/leaderboards HTTP/1.1Host: api.fanzava.comX-API-Key: fz_live_a1b2c3d4e5f6g7h8:AbCdEf...The two are equivalent. Send one, not both.
The fz_ prefix is what tells an API key apart from a browser session token, and the part before the colon is the lookup handle. A key sent without its prefix or without its colon is not recognised as a key at all, and the request is refused as unauthenticated rather than as a bad key.
Scopes
Section titled “Scopes”Scopes are a closed vocabulary. A key may hold any combination of these:
| Scope | Grants |
|---|---|
read:competitions |
Read competitions, rounds and fixtures |
write:competitions |
Create and update competitions |
read:tips |
Read submitted tips |
write:tips |
Submit and amend tips |
read:leaderboards |
Read leaderboards and standings |
read:achievements |
Read achievements and badges |
write:achievements |
Trigger achievement evaluation, which can award badges |
read:users |
Read user records visible to the hub |
read:members |
Read hub membership |
write:members |
Invite members and import a roster |
read:webhooks |
Read webhook subscriptions |
write:webhooks |
Create, update and delete webhook subscriptions |
read:hub |
Read hub settings and branding |
write:hub |
Update hub settings and branding |
A request for a route the key has no scope for is refused with HTTP 403 and Missing required scope: <scope>.
write:achievements reads like a refresh and is not one: it inserts achievement records and dispatches the resulting events. It sits under write for that reason.
What your plan allows
Section titled “What your plan allows”API access is a paid capability, and the plan sets both the depth of access and the request ceiling.
| Plan | API access | Requests per minute |
|---|---|---|
| Free | ✗ | 0 |
| Starter | ✗ | 0 |
| Growth | Read-only | 60 |
| Pro | Full | 300 |
| Scale | Full | 1,000 |
| Enterprise | Full | 10,000 |
These are the six plans on the Fanzava pricing page, which is the authority on plan names, prices and caps. Where a docs page disagrees with it, the pricing page is right.
Two things follow from that table.
A plan with no API access refuses a valid key with HTTP 402 and PAYMENT_REQUIRED, not 401. The credential is fine; the plan is the problem. The response carries an upgrade link so you are not left rotating a key that could never have worked.
On a read-only plan, write scopes are stripped from the key on every request. Narrowing happens at authentication rather than at issue, so a downgrade takes effect immediately and an upgrade restores the scopes already stored on the key without you reissuing it. The key list in the console shows the scopes stored against a key and the scopes your current plan actually permits, side by side.
Rate limits
Section titled “Rate limits”The ceiling in the table above is measured over a rolling 60 second window, per key, not per hub. Two keys on the same hub each get the full ceiling.
Once your key has been accepted and your plan checked, every response carries the current state, the 429 included:
X-RateLimit-Limit: your plan’s ceilingX-RateLimit-Remaining: requests left in this windowX-RateLimit-Reset: Unix timestamp when the window resets
Over the ceiling, the API answers HTTP 429 with the error code RATE_LIMIT_EXCEEDED. Read the reset header and back off rather than retrying immediately.
Do not treat the headers as guaranteed on every response. A request refused before the limiter runs carries none of them: that is the 401 for a credential we do not recognise, and the 402 for a plan without API access. Both are decided while authenticating, which happens first. Everything past that point is counted and carries the headers, HUB_CONTEXT_MISSING included. Read them when they are there; never require them.
Response shape
Section titled “Response shape”Every response, success or failure, uses the same envelope.
{ "success": true, "data": { }, "meta": { }}{ "success": false, "error": { "code": "FORBIDDEN", "message": "Missing required scope: read:leaderboards" }, "meta": { }}Branch on success and read error.code, not the message. Messages are written for a human reading a log and may be reworded.
Errors you should handle
Section titled “Errors you should handle”| Status | Code | What it means |
|---|---|---|
| 400 | HUB_CONTEXT_MISSING |
The route needs a hub id in the path and did not get one |
| 401 | UNAUTHORIZED |
No credential, or a credential that was not recognised |
| 402 | PAYMENT_REQUIRED |
The hub’s plan does not include API access |
| 403 | FORBIDDEN |
The key is valid but lacks the scope for this route |
| 404 | NOT_FOUND |
The resource does not exist, or belongs to another hub |
| 429 | RATE_LIMIT_EXCEEDED |
Over the plan’s request ceiling for this window |
A resource in another hub answers 404, never 403. That is deliberate: a 403 would confirm the resource exists.
Keeping keys safe
Section titled “Keeping keys safe”- Store the key in a secret manager, never in source control, a client-side bundle or a mobile app. Anything running in a browser or on a device can be read.
- Issue one key per integration. Revoking a shared key takes down everything that uses it.
- Give each key the narrowest scope set that works. A read-only integration should hold no write scope.
- Rotate on a schedule: create the replacement, cut the integration over, then revoke the old key.
- Set an expiry on anything short-lived, such as a migration script or a contractor’s access.