Authentication
Authenticated API requests pass a JWT token in the Authorization: Bearer <token> header — without one, only public data (entities with _sharing: public) can be read. Tokens are valid for 12 hours. The auth response includes an expires field (ISO 8601 datetime) so you know when to refresh.
Getting a Token
Every authentication method ends the same way: exchange a credential at GET /api/auth for a JWT token, then use that token on all subsequent requests.
API Key
API keys are long-lived credentials suited for scripts, CI/CD pipelines, and server-to-server integrations. Generate a key from any entity that has the entu_api_key property — typically your person entity — then exchange it for a token:
curl -X GET "https://entu.app/api/auth" \
-H "Authorization: Bearer YOUR_API_KEY"INFO
To restrict the resulting JWT to a single database, add ?db=mydbname to the auth request. The ?account=mydbname spelling is also accepted and behaves identically.
WARNING
The generated API key is displayed only once. Copy and store it securely — only its hash is stored and it cannot be retrieved again.
An entity can have multiple API keys. Delete individual keys when they are no longer needed.
OAuth
For interactive sessions, redirect users to /api/auth/{provider}. The provider authenticates the user and returns a temporary token. Exchange it at GET /api/auth:
curl -X GET "https://entu.app/api/auth" \
-H "Authorization: Bearer TEMPORARY_OAUTH_TOKEN"Supported providers: passkey, apple, google, e-mail, smart-id, mobile-id, id-card
Without a next URL (see Third-Party App Integration), the temporary token comes back as JSON { "key": "..." }. Add lang=en or lang=et to set the language of the OAuth.ee sign-in page; without it OAuth.ee chooses. It does not apply to passkey.
The provider returns a user ID and profile info that is matched against the entity's entu_user property. On first login, a person entity can be created automatically — see Users → Automatic User Creation. A sign-in that matches no database still gets a token, with an empty accounts list; that token can create a new database.
To accept an invite, add invite={INVITE_TOKEN} when exchanging the temporary token: the sign-in is linked to the invited person entity, and without db the JWT is limited to the invite's database. If the sign-in is already linked to another person in that database, the invite is not accepted and the response includes "conflict": "invite". An invalid, expired or already used invite — or one for another database than db — is rejected with 400 Invalid or expired invite; a used invite only still signs in the person it was for.
A passkey stores no name, so after a passkey sign-in user.name is the person's name in the first database, alphabetically, that has one.
Authentication Flow
- Authenticate using your OAuth provider or API key
- Exchange the credential at
GET /api/authfor a JWT token - Use the JWT in
Authorization: Bearer <token>on all subsequent requests - Refresh before the 12-hour expiry (see Refreshing a Token)
WARNING
JWT tokens are bound to the IP address used when the token was issued. If your IP changes (e.g. switching networks, VPN, or mobile roaming), the token is immediately rejected with 401 Invalid token and you must re-authenticate. Cache tokens per IP context if your environment changes addresses frequently. Tokens from the OAuth server are the exception — they are not bound to an IP.
TIP
Cache the JWT and reuse it across requests. Exchanging the credential on every call is wasteful — only refresh when the token nears expiry.
Every Entu JWT carries a use claim naming what it is for. Only use: access tokens — from GET /api/auth, /api/auth/refresh and the OAuth server — open the REST API and MCP; a session token or an invite is refused there. Tokens issued before the claim was added have no use and are accepted until 2026-11-06.
Refreshing a Token
Instead of re-authenticating, exchange a still-valid (or recently expired) token for a fresh 12-hour one at GET /api/auth/refresh:
curl -X GET "https://entu.app/api/auth/refresh" \
-H "Authorization: Bearer YOUR_CURRENT_TOKEN"The response has the same shape as GET /api/auth — accounts, user, token, and expires. The signature and IP binding are enforced, and account access is re-validated against the databases — for a provider or passkey sign-in, databases are looked up again by identity, so the refreshed token includes databases joined or created since sign-in. If no database is accessible any more, refresh fails with 401 No accessible accounts. A token without an IP binding — one from the OAuth server — cannot be refreshed: it is rejected with 401 Invalid token.
Refresh keeps a session alive as long as you refresh regularly, but two limits apply:
- Idle limit (14 days) — measured from the presented token's own issue time. A token left unused (not refreshed) for more than 14 days is rejected with
401 Token too old, re-authenticate. A client that refreshes within each 12-hour window never hits this. - Absolute limit (30 days) — measured from your original sign-in, which the token carries unchanged through every refresh. Once that is over 30 days old, refresh is rejected with
401 Session expired, re-authenticateand you must sign in again — no matter how often you refreshed.
Third-Party App Integration
The OAuth flow supports a next parameter that lets an external application receive the token after the user completes authentication in Entu. This is the recommended approach for building apps that delegate sign-in to Entu.
Redirect the user to the provider URL with a URL-encoded next value:
/api/auth/{provider}?next=https://your-app.com/callback?key=After the user authenticates, the server appends the session token to the next value and redirects the browser there:
https://your-app.com/callback?key={SESSION_TOKEN}The session token is short-lived (5 minutes), single use, and bound to the user's browser IP. Your app's frontend must exchange it for a full JWT by calling GET /api/auth directly from the browser:
const response = await fetch('https://entu.app/api/auth', {
headers: { Authorization: `Bearer ${sessionToken}` }
})
const { token } = await response.json()The exchange must originate from the same browser that completed the login — server-side exchange will fail because the IP will not match.
Security note
Always validate the next URL in your app before using the token. Only accept HTTPS URLs and reject any redirect to an origin you do not control.
OAuth Server
Entu is also an OAuth 2.1 authorization server. Instead of handling the next round trip yourself, your app can use a standard OAuth library: the user signs in on Entu, your app receives a token, and no credentials ever pass through your code.
All OAuth endpoints live on the API origin https://api.entu.app — the entu.app/api/… alias cannot be used here, because discovery documents must sit at the root of the issuer.
INFO
Every authorization is scoped to one database. Pass the database name as db, or the full resource URL as resource with the database as its first path segment (e.g. https://mcp.entu.app/mydatabase). The resource host must be in the API's own domain; any other is ignored.
Discovery
GET https://api.entu.app/.well-known/oauth-authorization-serverReturns the endpoint URLs. Most OAuth libraries fetch this for you.
Register a client
Clients register themselves — there is no application form and no client secret.
curl -X POST "https://api.entu.app/auth/register" \
-H "Content-Type: application/json" \
-d '{ "client_name": "My App", "redirect_uris": ["https://your-app.com/callback"] }'redirect_uris takes 1 to 10 absolute URIs of any scheme, so native apps can use a custom one; client_name is optional and kept to 200 characters. The returned client_id carries its own redirect URIs and stays valid for a year. Store it — registering again on every start creates a new one needlessly.
Authorize
Send the user's browser to:
https://api.entu.app/auth/authorize
?client_id={CLIENT_ID}
&redirect_uri=https://your-app.com/callback
&response_type=code
&code_challenge={CHALLENGE}
&code_challenge_method=S256
&state={STATE}
&db={DATABASE}PKCE is required and only S256 is accepted. The user signs in on Entu, and your redirect_uri then receives code and state.
Without provider, OAuth.ee asks the user which provider to use. To skip that choice, add &provider={PROVIDER} with one of the supported providers — for example &provider=passkey sends the user straight to the passkey sign-in. A passkey sign-in starts only this way.
An unknown client_id or a redirect_uri not registered for it is answered with 400. Once both check out, any other problem — wrong response_type, missing PKCE, no database, unknown provider — is sent back to your redirect_uri as error and error_description, with your state.
Exchange the code
curl -X POST "https://api.entu.app/auth/token" \
-d "grant_type=authorization_code" \
-d "code={CODE}" \
-d "redirect_uri=https://your-app.com/callback" \
-d "code_verifier={VERIFIER}"{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 43200
}The access_token is an ordinary Entu JWT — use it exactly as described above.
INFO
Unlike other Entu tokens, this one is not bound to an IP address — your backend can exchange the code and the token works from any machine. For the same reason it cannot be renewed at /api/auth/refresh (see Refreshing a Token).
Codes are single use and expire after five minutes. When the token expires, run the flow again.
Auth Properties
Authentication credentials are stored as properties on an entity. By default these are used on person entities — each person entity represents a human user. But the same properties can be added to any entity type, which lets non-human actors authenticate too. A robot entity in an IoT setup, a screen entity in a digital signage system, or a service entity for a backend integration can all have their own API key and authenticate independently.
The stored values are used only for signing in. Everywhere else — API responses, GraphQL, filters, sorting, search, formulas and history — a credential shows only its masked form:
| Property | Shown as |
|---|---|
entu_user | string: the login's email (ID-card logins without @eesti.ee) or a passkey's device label; provider; invite: true while an invite is pending |
entu_api_key | string: *** |
The only exceptions are the create and edit responses: a new API key is returned once in plain text, and an invite token is returned only to the person it is for (Add Login Method).
entu_user
- Stores one login:
providerand the provider user ID asuid, along with other info returned by the OAuth provider (such as email) - Set automatically when a new person entity is created on first login
- Writing it with any
stringstores an invite instead, valid 24 hours; on an existing entity the valuesend-invitealso emails the invite link to the entity'semail(400 No emailwithout one) — see Users → Adding Users - Login fields are set only by a sign-in — accepting an invite (including Add Login Method on one's own person), automatic user creation or creating a new database — never written directly; one entity can have several logins
- Can be deleted like any property value
entu_api_key
- Create the property with any
stringvalue (for example{ "type": "entu_api_key", "string": "generate" }) — Entu discards it and generates a cryptographically secure 32-character key - The hash is stored; the plain key is returned only once, as
stringin the create response - Only the entity's
_owneror the entity itself can add it - Multiple keys can exist on the same entity
