Client tokens
Your backend mints short-lived client tokens with its API key so apps never hold the key. How authentication works describes the endpoint your backend adds: mint only for a signed-in user, and take end_user_id from that user's session, never from the request.
Client-token scopes
A client token can call only the operations its scopes allow; anything else fails with 403 scope_insufficient. An app's January client uses one token for every call, so request the scopes of every feature your app uses, and no others.
foods:read
Foods: autocomplete, search, barcode lookup, get a food, and alternatives
restaurants:read
Restaurants: restaurant search, menu-item search, and a restaurant's menu
food_analysis:write
Food analysis: image, text, and corrections
food_logs:read
Food logs: list, summary, and get
food_logs:write
Food logs: create, update, and delete
water_logs:read
Water logs: list daily totals
water_logs:write
Water logs: create and delete
weight_logs:read
Weight logs: list daily weights
weight_logs:write
Weight logs: create
glucose:read
Minting and revoking client tokens, and reading Credits, accept only an API key.
Each operation's description states what it costs — see Credits and pricing.
API key only.
Never consumes credits.
Exchanges your partner API key for a short-lived token bound to one of your end users, so your mobile app can call the v1.2 API directly instead of through a proxy that holds your key.
This endpoint requires your API key (sk-…), so always call it from your backend — behind whatever login already protects your own APIs. Never ship your API key in a mobile app in order to call this from the device: that puts a credential for your whole account in every copy of your app, which is the problem client tokens exist to solve.
The end user is identified by end_user_id in the body, not the January-End-User-ID header: the token is bound to it, and requests made with the token act only on that user, whatever headers they carry.
Tokens last 300–7200 seconds and the raw value is returned exactly once — it is stored only as a hash, so it can never be retrieved again. Relay it to the device and let the device refresh when it expires: a 401 with code token_expired is the signal to mint a new one and retry the original request once.
Your credential, sent as Authorization: Bearer <credential>. Two kinds reach this API, and the prefix tells them apart.
API key (sk-…) — created in the Developer Dashboard, it authenticates your whole account. Use it only where you control the machine — never inside a mobile app or a browser. Every endpoint accepts it.
Client token (ct-…) — a short-lived credential bound to one of your end users, minted from your API key by POST /v1.2/auth/client-tokens on your backend and relayed to the device. Safe to hold on a phone: it expires within 2 hours and acts only as the user it was minted for. Endpoints that open with API key or client token accept one and name the scope it must carry; endpoints that open with API key only refuse it.
Your stable ID for the end user this token acts as. The token is bound to it; requests made with the token act only on this user.
acme-user-8271How long the token stays valid, in seconds. Between 300 and 7200; defaults to 1800.
1800Example: 1800Token minted. The raw token is returned exactly once — relay it to the device and store nothing; only its hash is kept. The response also echoes expires_in/expires_at, the bound end_user_id, and the exact scopes granted.
The credential itself. Shown exactly once — it is stored only as a hash and can never be retrieved again.
ct-4fQr7yNb2KcXm9TvLpZ3wHs6JdRg8AeYuQ1oViB5xCnSeconds until the token expires, counted from the moment this response was produced. Compute expiry from this rather than from expires_at — a device clock that is wrong makes an absolute timestamp wrong with it.
1800The same expiry as an absolute UTC instant, for logs and humans.
2026-08-26T18:35:11.000ZThe end user this token is bound to, echoed back so a caller can assert it minted what it meant to.
acme-user-8271The request carried no Authorization header, or the credential in it was rejected.
unauthorized — the header is missing or malformed, or the key is not one we recognise. A valid key belonging to the other API version is 403 forbidden instead.
A client token is rejected in one of three ways, and only the first should be handled automatically:
token_expired— the token is past its TTL. Mint a fresh one from your backend and retry the request once. This is routine and expected once per TTL window.token_invalid— no such token: it was never issued, or it has been purged, which happens shortly after it expires. This code is not an automatic token-refresh signal.token_revoked— the token was revoked, byPOST /v1.2/auth/client-token-revocationsor from the dashboard. The end user signs in again in your app, and your backend decides whether to mint another; a device that just re-mints defeats the revocation.
forbidden — client tokens have been switched off for your account (turn them back on in the Developer Dashboard), or the key is issued for the other API version.
client_token_not_allowed — this request was made with a client token, and minting requires your sk- API key.
Minting is capped per partner (code: rate_limited) — the only limit that applies here. It is @NotBillable and outside the shared v1.2 abuse cap, so this 429 never means credit exhaustion or the partner-wide abuse cap. Honor Retry-After when present, otherwise back off a few seconds; mint one token per user session and reuse it until it expires rather than minting per request.
Client tokens are not configured on this environment.
Any other error: the HTTP status plus { code, message }. Retry only rate_limited and the transient 5xx codes.
POST /v1.2/auth/client-tokens HTTP/1.1
Host: partners.january.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 75
{
"end_user_id": "acme-user-8271",
"scopes": [
"foods:read"
],
"ttl_seconds": 1800
}{
"token": "ct-4fQr7yNb2KcXm9TvLpZ3wHs6JdRg8AeYuQ1oViB5xCn",
"expires_in": 1800,
"expires_at": "2026-08-26T18:35:11.000Z",
"end_user_id": "acme-user-8271",
"scopes": [
"foods:read",
"food_logs:read"
]
}API key only.
Never consumes credits.
Revokes every outstanding client token for one end user. Safe to call repeatedly: it reports how many tokens it actually stopped, so an immediate second call reports 0.
This endpoint requires your API key (sk-…), so always call it from your backend. A client token cannot revoke anything, its own included.
Revocation takes effect within 60 seconds, the authentication cache window. For an immediate cut-off, stop trusting the user in your own app as well — and note that a token minted in the same instant as the revoke may survive it, bounded by its own expiry.
If some tokens cannot be revoked, the call answers 503 with code client_token_revocation_incomplete, naming how many of them succeeded rather than reporting a smaller count as though it were the whole story — retry until it succeeds, which only picks up the remainder.
One call stops at most 500 tokens so it cannot time out, so repeat the call until revoked_count is 0 if a user is somehow holding more than that (which means your app is minting per request rather than per session).
Your credential, sent as Authorization: Bearer <credential>. Two kinds reach this API, and the prefix tells them apart.
API key (sk-…) — created in the Developer Dashboard, it authenticates your whole account. Use it only where you control the machine — never inside a mobile app or a browser. Every endpoint accepts it.
Client token (ct-…) — a short-lived credential bound to one of your end users, minted from your API key by POST /v1.2/auth/client-tokens on your backend and relayed to the device. Safe to hold on a phone: it expires within 2 hours and acts only as the user it was minted for. Endpoints that open with API key or client token accept one and name the scope it must carry; endpoints that open with API key only refuse it.
The end user whose outstanding client tokens should all be revoked.
acme-user-8271Revocation processed. revoked_count is how many live tokens this call actually stopped — repeat the call until it reports 0, since already-revoked and already-expired tokens are not counted.
How many live tokens this call revoked. Already-revoked and already-expired tokens are not counted, so an immediate second call reports 0 — repeat the call until it does.
2The request carried no Authorization header, or the credential in it was rejected.
unauthorized — the header is missing or malformed, or the key is not one we recognise. A valid key belonging to the other API version is 403 forbidden instead.
A client token is rejected in one of three ways, and only the first should be handled automatically:
token_expired— the token is past its TTL. Mint a fresh one from your backend and retry the request once. This is routine and expected once per TTL window.token_invalid— no such token: it was never issued, or it has been purged, which happens shortly after it expires. This code is not an automatic token-refresh signal.token_revoked— the token was revoked, byPOST /v1.2/auth/client-token-revocationsor from the dashboard. The end user signs in again in your app, and your backend decides whether to mint another; a device that just re-mints defeats the revocation.
forbidden — the key is issued for the other API version. Unlike minting, revocation is not gated on your account’s client-token switch (Developer Dashboard): switching minting off must never take away the ability to revoke what is already out there.
client_token_not_allowed — this request was made with a client token, and revocation requires your sk- API key.
Revocation is capped per partner (code: rate_limited) — the only limit that applies here. It is @NotBillable and outside the shared v1.2 abuse cap, so this 429 never means credit exhaustion or the partner-wide abuse cap, and a partner mid-incident can still cut a device off. Honor Retry-After when present, otherwise back off a few seconds, and retry; the revoke bucket is separate from minting, so a security sweep cannot spend your sign-in allowance.
service_unavailable — client tokens are not configured on this environment. client_token_revocation_incomplete — some tokens could not be revoked; the message names how many succeeded, and repeating the request is safe and picks up the remainder.
Any other error: the HTTP status plus { code, message }. Retry only rate_limited and the transient 5xx codes.
POST /v1.2/auth/client-token-revocations HTTP/1.1
Host: partners.january.ai
Authorization: Bearer YOUR_SECRET_TOKEN
Content-Type: application/json
Accept: */*
Content-Length: 32
{
"end_user_id": "acme-user-8271"
}{
"revoked_count": 2
}Last updated
Was this helpful?