API auth and keys
Two credentials, one hard boundary: tenant-scoped API keys with mint-time scopes stay on your server, and short-lived participant JWTs are the only thing the browser sees. This page covers the key model, the token mint flow, rate limits, and rotation.
Key takeaways
Quick read- API keys go in the X-API-Key header, server-side only. Default scopes: events:write plus gamify:read.
- Value movement (award, deduct, assign, claim) needs gamify:write, granted explicitly at mint. Token minting and webhook management need admin scope.
- The browser gets a participant JWT only: minted server-side, bound to one participant, default TTL 5 minutes, max 60.
- Two environments by key prefix: bq_test_ resolves to your isolated test sibling tenant, bq_live_ to production.
- Rate limits: 1,000/min per key (default), 5,000/min per tenant, 100 events/min per participant. Respect Retry-After on 429.
The model
One key type, mint-time scopes, two environments
| Credential | Scopes | Use for | Where it lives |
|---|---|---|---|
| bq_live_... (default scopes) | events:write, gamify:read | Emit events and read participant state from your backend. The standard integration loop. | Server only. |
| bq_live_... (+ gamify:write) | adds value movement | Award points, assign tiers, award badges, record streaks, claim rewards from trusted backend flows. | Server only. |
| bq_live_... (+ admin) | everything | Webhook destination CRUD and participant-token minting. | Server only, ideally a dedicated service. |
| Participant token (JWT) | read:state, write:events | Browser and mobile: read own state, emit own events, open the SSE stream. | Client, expires in minutes. |
Sending the key
X-API-Key on every server request
curl https://api.bricqs.co/api/v1/gamify/state/user_42 \
-H "X-API-Key: bq_live_aV7p..."The tenant is resolved from the key itself; there is no separate tenant header.
const BRICQS_API = "https://api.bricqs.co/api/v1";
export async function bricqs<T>(
path: string,
init: RequestInit = {}
): Promise<T> {
const res = await fetch(`${BRICQS_API}${path}`, {
...init,
headers: {
"X-API-Key": process.env.BRICQS_API_KEY!,
"Content-Type": "application/json",
...init.headers,
},
cache: "no-store",
});
if (!res.ok) {
const body = await res.json().catch(() => null);
// Standard envelope: { error: { type, code, message, request_id } }
throw new Error(
`Bricqs ${res.status} ${body?.error?.code ?? ""}: ${body?.error?.message ?? "unknown"}`
);
}
return res.json();
}Participant tokens
Short-lived JWTs for client code
Browsers and mobile apps never see the raw API key. Your backend trades an admin-scoped key for a participant-bound JWT and hands only the JWT to the client.
import { mintParticipantToken } from '@bricqs/sdk-server';
export async function GET() {
const { token, expiresAt } = await mintParticipantToken({
adminApiKey: process.env.BRICQS_ADMIN_API_KEY!, // admin-scoped key
participantId: 'user_42', // your stable user ID
expiresInMinutes: 30, // default 5, max 60
});
return Response.json({ token, expiresAt });
}# A standard bq_live_... key; "admin" is a SCOPE on the key, not a
# different key format. Keys are always bq_live_/bq_test_ + 32 hex chars.
curl -X POST https://api.bricqs.co/api/v1/auth/participant-token \
-H "X-API-Key: bq_live_..." \
-H "Content-Type: application/json" \
-d '{
"participant_id": "user_42",
"expires_in_minutes": 30
}'
# response
{
"token": "eyJhbGciOi...",
"participant_id": "user_42",
"tenant_id": "…",
"expires_at": "2026-07-19T11:12:00Z"
}The token is pinned to one participant: it reads that participant's state (read:state) and emits that participant's events (write:events), nothing more. Requests for another participant return 403. There is no refresh token; re-mint before expires_at.
Rate limits
The limits and how to handle them
Per-key limit
1,000 requests/minute by default, configurable per key at creation. 429 carries Retry-After plus X-RateLimit-* headers and code RATE_LIMIT_API_KEY_EXCEEDED.
Per-tenant limit
5,000 requests/minute aggregate across all keys, so one runaway service cannot starve the tenant.
Ingestion burst
100 requests/second IP-level burst guard on the event endpoints, plus the per-participant cap of 100 events/minute (batched events count individually).
Backoff strategy
On 429: respect Retry-After first, then exponential backoff with jitter. Event retries are safe thanks to idempotency keys.
Rotation
Rotate without downtime
When to rotate:
- On any suspected leak, immediately.
- On personnel change for anyone with key access.
- Periodically as your security policy dictates.
Process (dashboard: Settings -> API Keys):
1. Create a new key with the SAME scopes as the old one.
2. Deploy it to the service (one key per service makes this surgical).
3. Watch the old key's last_used_at stop advancing.
4. Deactivate the old key. In-flight traffic during cutover never 401s
because both keys were valid through the window.
Webhook destination secrets rotate differently: there is no rotation
endpoint, so create a second destination with the same URL, verify
deliveries, then delete the old one. See /docs/webhooks.Common mistakes
The mistakes that leak keys
Putting an API key in NEXT_PUBLIC_ env. Now it is in the JS bundle.
Server-only env names (BRICQS_API_KEY, BRICQS_ADMIN_API_KEY). The browser gets participant tokens via your mint route.
Using one admin-scoped key for everything, everywhere.
Default-scope keys for emit-and-read services; gamify:write only on the service that moves value; admin only on the token-mint and webhook-management path. One key per service.
Reusing the live key in test. Test traffic pollutes production data.
bq_test_ keys hit the isolated test sibling tenant. Assert the expected prefix on service boot.
Minting hour-long participant tokens to avoid writing refresh logic.
60 minutes is the hard server cap, but short TTLs are the point. Use the SDK's getToken callback and re-mint on demand.
Treating 403 as an auth outage and retrying.
403 means the credential works but lacks the scope (most often gamify:write) or a participant token crossed identities. Fix scopes; retrying never helps. Branch on error.code from the envelope.
Developer FAQ
Common questions when integrating gamification with Bricqs.
Ready to ship?
Wire it up with the Bricqs SDK or API
Headless SDK for React UIs, REST API for any backend. Same engine behind both.
