Management API
The management API is how you administer fremai without a browser: issue and revoke inference keys, read usage, read the credit ledger, and rotate your own credentials.
It is a separate surface from the inference API, with a separate base URL and a separate kind of credential.
| Inference API | Management API | |
|---|---|---|
| Base URL | https://api.fremai.eu/v1 | https://app.fremai.eu/_api/v1 |
| Credential | sk-fremai-… | fma_… |
| Purpose | Run models | Administer the account |
The two credentials are deliberately not interchangeable. Presenting an sk-fremai-… inference key here returns a named error telling you so, rather than a generic 401 — that mistake is common enough to be worth its own message.
Getting a token
Management tokens are created in the console: Settings → Management tokens. The first one has to be made there; after that a token with tokens:read can enumerate your tokens so automation can rotate itself.
The token value is shown once, at creation. Only its SHA-256 is stored, so it cannot be recovered — if you lose it, revoke it and mint another.
Scopes
A token carries an explicit set of scopes. They are coarse on purpose: a fine-grained scope is a promise about enforcement at every call site, and an unenforced scope is worse than an absent one, because it is believed.
| Scope | Grants |
|---|---|
keys:read | List inference keys |
keys:write | Issue and revoke inference keys |
usage:read | Read usage |
billing:read | Read the credit ledger and receipts |
tokens:read | List your own management tokens |
keys:write does not imply keys:read. Implication is a rule the reader has to know, and the only thing it buys is a shorter token definition. A CI token that may rotate keys but not enumerate them is a legitimate and useful shape, so grant exactly what you need.
Authentication
curl https://app.fremai.eu/_api/v1/keys \
-H "Authorization: Bearer $FREMAI_MANAGEMENT_TOKEN"There is no tenant in the path. A management token is issued for one tenant and carries it, so a slug in the URL would be a second source of truth and a class of “token for A used against B’s path” bugs to defend against. There is nothing to defend if the path cannot express it.
Endpoints
GET /keys
Requires keys:read. Lists the tenant’s inference keys. Never returns key material — only the last four characters.
{
"data": [
{ "id": "…", "name": "ci", "last4": "AB12", "status": "active", "createdAt": "2026-08-01T09:00:00.000Z" }
]
}status is one of active, revoked, expired. A revoked key reports revoked even if it had also expired — revocation is a deliberate act and the more useful thing to know about a key that stopped working.
POST /keys
Requires keys:write. Issues a new inference key.
curl -X POST https://app.fremai.eu/_api/v1/keys \
-H "Authorization: Bearer $FREMAI_MANAGEMENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "ci"}'{
"data": { "id": "…", "name": "ci", "key": "sk-fremai-…", "last4": "AB12" },
"note": "Store `key` now — it is hashed on our side and cannot be shown again."
}Returns 201. key is returned exactly once, for the same reason management tokens are: only the hash is stored.
An optional profile selects a key profile where your plan offers them.
DELETE /keys/{id}
Requires keys:write. Revokes a key. Returns 204 with no body.
A key belonging to another tenant returns 404, not 403. Confirming that an id exists but belongs to someone else is a cross-tenant existence oracle, and the difference is not something you could act on anyway.
GET /usage
Requires usage:read. Returns the tenant’s usage summary.
GET /billing
Requires billing:read. Returns the current credit balance, the credit ledger, and receipts.
{
"data": {
"balanceCents": 4200,
"entries": [
{ "type": "topup", "amountCents": 5000, "source": "mollie", "ref": "tr_…", "createdAt": "2026-08-01T09:00:00.000Z" }
],
"invoices": [
{ "period": "2026-08", "amountCents": 5000, "status": "paid", "dineroNumber": 1234, "createdAt": "2026-08-01T09:00:00.000Z" }
]
}
}All amounts are integer cents, never floats. Ledger entries are signed: a top-up is positive, a debit negative.
GET /tokens
Requires tokens:read. Lists the tenant’s management tokens — this one included.
Never returns a token value or its hash. The raw token existed once, at creation. This endpoint answers “what credentials does my tenant have, and which are stale”, which is what rotation needs:
{
"data": [
{
"id": "…", "name": "ci", "last4": "9F3A",
"scopes": ["keys:read", "keys:write"],
"createdAt": "2026-08-01T09:00:00.000Z",
"lastUsedAt": "2026-08-18T06:00:00.000Z",
"expiresAt": null,
"revokedAt": null
}
]
}lastUsedAt is what makes rotation safe: mint the replacement, move your automation over, then confirm the old token has stopped being used before revoking it.
Errors
Every error carries a stable machine-readable code:
{ "error": { "code": "insufficient_scope", "message": "This token lacks the scope required for this endpoint." } }| Status | code | Meaning |
|---|---|---|
| 400 | name_required | name is required when issuing a key |
| 401 | missing_credential | No Authorization: Bearer fma_… header |
| 401 | invalid_token | Not a token we recognise |
| 401 | token_expired | The token has expired |
| 401 | token_revoked | The token was revoked |
| 401 | inference_key_not_accepted | An sk-fremai-… key was presented here |
| 403 | insufficient_scope | The token is valid but lacks the scope |
| 404 | not_found | No such key |
invalid_token deliberately covers both an unknown token and a malformed one. Telling a caller that a well-formed token was merely not found confirms the format to anyone probing it.
Responses are JSON only. Unlike the console, this surface does no content negotiation — it has exactly one kind of caller.
Rotating a token
The pattern the API is built for:
- Mint the replacement in the console, with the same scopes.
- Move your automation to the new token.
GET /tokensand check the old token’slastUsedAthas stopped advancing.- Revoke the old token in the console.
Step 3 is the one worth not skipping. Revoking a credential you believe is unused is how a pipeline breaks at 03:00.