Authentication
How to obtain a bearer token, what it scopes, and how server-to-server access works.
Every endpoint except /health requires a bearer token:
Authorization: Bearer <token>Tokens are Clerk session JWTs. Flambe does not issue its
own API keys to third parties — the token's sub claim is the Flambe user id,
and it is what scopes every request. A token can only ever see and modify what
that user can see and modify.
From a browser or mobile client
If you already use a Clerk SDK, ask it for the current session token:
import { useAuth } from '@clerk/nextjs';
const { getToken } = useAuth();
async function authedFetch(path: string, init: RequestInit = {}) {
const token = await getToken();
return fetch(`https://api.flambe.dev${path}`, {
...init,
headers: {
...init.headers,
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
});
}getToken() returns a short-lived token and refreshes it as needed. Call it per
request rather than caching the string — that is the single most common cause of
spurious 401s.
From a server or a script
Use a Clerk backend SDK to mint a token for the user you are acting as. Never ship a Clerk secret key to a client.
import { createClerkClient } from '@clerk/backend';
const clerk = createClerkClient({ secretKey: process.env.CLERK_SECRET_KEY! });
// Exchange a session for a short-lived JWT.
const { jwt } = await clerk.sessions.getToken(sessionId, 'default');For a long-running integration with no interactive user, create a dedicated Flambe account for it and drive that account's sessions.
Reading the 401
The two failures mean different things and want different handling:
| Body | Meaning | What to do |
|---|---|---|
{"error":"Access token required"} | The Authorization header was absent entirely. | Fix the client. Do not refresh — there was no session to refresh. |
{"error":"Invalid or expired token"} | The header arrived but the JWT did not verify. | Refresh the session and retry once. |
Treating Access token required as an expired session sends clients into a
refresh loop that can never succeed, because the header was never being sent
in the first place. Branch on the message.
Verifying a token
GET /api/users/me is the cheapest way to check a token and learn who it
belongs to:
curl https://api.flambe.dev/api/users/me \
-H "Authorization: Bearer $FLAMBE_TOKEN"{
"id": "user_2abcDEF",
"email": "you@example.com",
"household_ids": ["1f0e2d3c-4b5a-6978-8796-a5b4c3d2e1f0"]
}Endpoints that need no token
Two, deliberately:
GET /health— the liveness probe.GET /api/shared/{token}— resolves a public share link so a recipient without an account can view it.
Everything else, including POST /api/shared/{token}/save, is authenticated.
Legacy username/password
POST /api/auth/register and POST /api/auth/login still exist for older
clients and are marked deprecated. New integrations should not use them.