Skip to content

Authentication ​

All API requests to FAST require authentication. The method depends on the caller type.

API Keys ​

Every tenant receives three credentials during onboarding:

KeyPrefixPurposeWhere to use
Server API Keykey-...Full server-side access (bets, purchases, withdrawals, admin, minting user tokens)Your backend only
Public Widget Keypk_...Tenant-level reads only (tiers, terms, leaderboard, loyalty tiers, widget config)Frontend / embedded widget (safe to expose)
Webhook Secret(64-character hex string, no prefix)HMAC-SHA256 verification of inbound webhooks and approval callsYour backend

You start with a staging tenant for testing. When ready, a production tenant is created with fresh keys.

bash
# Backend .env — server key + webhook secret never reach the browser
FAST_API_KEY=key-abc123...
FAST_WEBHOOK_SECRET=9f2c4e...a71b   # 64 hex characters
# public key is the ONLY credential that may appear in frontend HTML
FAST_WIDGET_KEY=pk_xyz789...

Using the server API key ​

Pass the key in the X-API-Key header on every request:

X-API-Key: key-your-production-key

The server key is unrestricted: it runs the full server-to-server flow and can act on any user in your tenant.

Public widget key: what it can and cannot do ​

The pk_... key is embedded in the browser, so it is public and shared by every visitor. It therefore grants tenant-level reads only:

  • GET /v1/challenge-tiers, GET /v1/challenge-tiers/:id/terms
  • GET /v1/leaderboard (anonymized)
  • GET /v1/loyalty/tiers
  • GET /v1/widget/config

The public key cannot, on its own:

  • access any /v1/users/:id/* resource (returns 403 — user data needs a user token)
  • ingest bets (POST /v1/bets, /bets/batch are server-key only)
  • access /v1/admin/* (returns 403)

To read or act on a specific user from the browser, the request must additionally carry a user token that is bound to that user.

Key security ​

Keys are stored as SHA-256 hashes. FAST never stores plaintext keys. If a key is compromised, request rotation through the super admin console or contact support@fundedfast.eu.

User Token ​

A user token binds a browser request to one specific userId. It is how the plug-and-play widget makes user-scoped calls without shipping a powerful key to the browser.

Your backend mints it (after it has authenticated the player in its own session) using the server key:

bash
curl -X POST https://api.fundedfast.eu/v1/sse/token \
  -H "X-API-Key: key-your-server-key" \
  -H "Content-Type: application/json" \
  -d '{"userId": "player-42"}'
json
{ "token": "eyJ0aWQ...", "expiresAt": "...", "ttl": 3600, "userId": "player-42" }

The token is an HMAC-signed {tenantId, userId, expiry} (1-hour TTL). Send it:

  • on REST calls as the X-User-Token header, alongside the pk_ key
  • on SSE as ?token=<token>&userId=player-42
  • to the widget as the data-user-token attribute

FAST enforces token.uid === the userId being accessed, so a token for player-42 can only ever touch player-42. This is what prevents anyone who copies the public key from reading or acting on other users.

Minting is a trusted operation: only a server key (or an existing user token refreshing its own userId) may mint. On sandbox/staging tenants the public key may also self-mint as a convenience for demos; in production this is refused — the operator backend must inject data-user-token.

Token lifetime & refresh ​

  • TTL is 1 hour. The widget renews the injected token automatically: shortly before expiry it exchanges the still-valid token for a fresh one, so open tabs keep working without any host-page code.
  • Self-refresh chains are capped at 24 hours from the original mint (iat travels with the token). Past the cap, POST /v1/sse/token returns 403 token_chain_expired and only your backend (server key) can mint again.
  • onTokenExpired (widget config) fires — at most once per 30s — only when the widget can no longer renew the token itself (chain cap hit, or the token was already expired). Mint a fresh token from your backend and call widget.update({ userToken }).

Widget money actions require operator approval ​

For actions triggered from the widget — buy, withdraw, refund, cancel — FAST additionally calls your single configured approvalUrl (the action type is in the payload and the X-Fast-Event header) and only proceeds on { "approved": true }. On production tenants, if the URL is not configured, a widget-initiated action is refused with 403 approval_required. Staging/sandbox tenants auto-approve when no URL is set (so demos work without an approval endpoint). Actions performed directly by your backend (server key) go through the same approval call when an approvalUrl is set; with none set they proceed, because your backend already vouched for them. See Operator Approvals. See Challenge Purchase and Withdrawals.

Admin JWT ​

Operator backoffice users authenticate with email and password via POST /v1/auth/login. The response contains a JWT token valid for 1 hour.

bash
curl -X POST https://api.fundedfast.eu/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@example.com", "password": "your-password"}'

Use the token in the Authorization header for /v1/admin/* endpoints (the backoffice uses the equivalent httpOnly session cookie). Your backend can call the same /v1/admin/* endpoints with the server key in X-API-Key instead:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

If MFA is enabled, the login response returns mfaRequired: true. Complete authentication with POST /v1/auth/mfa/verify using the 6-digit code sent to your email. Use POST /v1/auth/mfa/send to resend the code (90-second rate limit).

Super Admin Key ​

FAST internal endpoints under /v1/super-admin/* require the X-Super-Admin-Key header or a JWT with the fast_admin role.

X-Super-Admin-Key: your-super-admin-key

Super admin endpoints have their own rate limit of 60 requests per minute per client IP. The tenant limits below do not apply to them.

SSE Token ​

Server-Sent Events (SSE) connections cannot send HTTP headers. They authenticate with the same user token described above — minted by your backend with the server key:

bash
curl -X POST https://api.fundedfast.eu/v1/sse/token \
  -H "X-API-Key: key-your-server-key" \
  -H "Content-Type: application/json" \
  -d '{"userId": "player-42"}'

Connect to SSE with the token (no key in URL):

GET /v1/events?token=<token>&userId=player-42

Tokens expire after 1 hour and are scoped to a single user. See Real-Time Events for full details.

Rate Limits ​

FAST enforces per-tenant rate limits using Redis-backed counters with in-memory fallback. Limits are per source IP (server-key traffic) unless noted otherwise, per 1-minute window.

Endpoint CategoryLimit
/v1/bets/validate3000/min
/v1/bets3000/min, plus 30 events per user per minute (see below)
/v1/users/*2000/min
/v1/challenges/*1000/min
/v1/admin/*300/min
/v1/auth/*20/min
/v1/audit/*200/min
/v1/reconciliation/*200/min
All other tenant endpoints1500/min

Bet ingestion (POST /v1/bets and each item of /v1/bets/batch) is also capped at 30 events per user per minute, placements and settlements combined. Over it, FAST returns 429 rate_limited without a Retry-After header. See Bet Events.

There is a second, higher cap per tenant across all source IPs combined (10,000/min) so no single tenant can exhaust shared capacity regardless of how many IPs it spreads traffic across.

Widget traffic authenticated with a user token is rate-limited per user, not per IP (default 600/min per endpoint category, env-tunable: WIDGET_USERS_RATE_LIMIT_PER_MIN, WIDGET_TIERS_RATE_LIMIT_PER_MIN, WIDGET_LEADERBOARD_RATE_LIMIT_PER_MIN, WIDGET_DEFAULT_RATE_LIMIT_PER_MIN), so one user cannot exhaust another's budget or bypass the limit by rotating IPs. A public pk_ key with no user token is still limited per source IP. The tenant-wide cap for widget traffic is separately configurable and defaults to 60,000/min.

Rate limit headers are included in every response:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1706000000

When a limit in the table is exceeded, FAST returns 429 Too Many Requests with a Retry-After header indicating seconds until the window resets. A short-lived burst allowance (100 requests, refilling at 20/s) additionally absorbs spikes, but only while Redis is unreachable and the limiter has fallen back to in-memory counting per API replica — under normal operation the table above is a hard cap with no burst on top.

Separately, a client IP that produces 30 failed authentications (401) within a minute is answered 429 with Retry-After: 60 on every authenticated request until the minute passes.

BLACKLINE CONSULTING d.o.o.