Your users
API keys
Let your users issue long-lived, revocable keys for an AI agent, a script or an app, with roles you allow. Turn it on and decide who may issue what.
An API key is a long-lived credential for something that acts for a user of your service: an AI agent, a script, a mobile app, another backend. It stays connected with no sign-in dialog to renew a token, and you can revoke each key on its own. AI agents over MCP are the main case; the key works the same for any client.
The key belongs to a user of your service, one of the people who sign in to your application. That user issues it, with roles you allow for that user’s role, and your application asks the service for it on the user’s behalf. As the owner, you decide whether keys exist at all and which roles a key may carry for whom. You see every key issued and can revoke any of them.
The personal access tokens of your Ulabase account are a different thing: ulabase signs in with them, and they reach your services' configuration. An API key reaches one service’s data, as one of its users, and nothing else.
How it works
-
You install the API Keys feature on the service, as you would any other, and set its default limits.
-
You write a permission on
/keysfor each role that may issue, naming in it the roles a key may be given. The API Keys page does this for you. -
A user issues a key with
POST /keys, signed in with their own credentials, naming the roles they want among those their permission allows. -
The client sends the key as
Authorization: Bearer ulak_…, and does what those roles may do. -
Anyone revokes: the user their own, you any.
A key can be issued only when the feature is enabled and a permission has an apiKeys block. Either one alone does nothing, and the error says which one is missing.
Who may issue which keys
You write this in the service’s ACL, like every other grant. A permission that lets a role reach /keys carries an apiKeys block with the roles a key may be given and, optionally, its own limits:
{ "_id": "adminsIssueKeys",
"predicate": "path-prefix('/keys')",
"roles": ["admin"],
"priority": 10,
"apiKeys": { "roles": ["cli-rw", "api-admin"], "max-expires-in-days": 365 } }
{ "_id": "usersIssueKeys",
"predicate": "path-prefix('/keys')",
"roles": ["user"],
"priority": 1,
"apiKeys": { "roles": ["cli-ro"] } }
With these, an admin may issue a cli-rw or an api-admin key, and a user only a cli-ro one. The key roles need not be roles the user holds, and usually they are not: a key’s role is narrower than its owner’s, and it can do only what other permissions grant to it. cli-ro, cli-rw and api-admin are ordinary roles you define.
Four rules follow from how RESTHeart evaluates permissions:
-
One permission decides. When several match, for a user who is both
adminanduser, the one with the highest priority applies, and only itsapiKeys.rolesare available. Give the wider grant the higher priority. -
A permission without the block grants nothing. Its roles reach
/keysand get a403that says so. -
A malformed block is refused when it is written.
rolesmust be a non-empty array of distinct role names, the two limits whole numbers within their bounds, and no other field is accepted. The service answers the write with a400that names the field, whoever writes it: the console,ulabaseorcurl. So a typo such asroleforrolesis caught at once, before a user is refused. -
An administrator never gets a key. A user with the role
rootorsrv-adminadministers the service, whatever the ACL says.POST /keysanswers403. When an agent signs them in, it gets their own token with all their permissions, or nothing when the service hands out only keys. To connect an agent with a key, sign in as a user of the service.
The API Keys page lists the permissions on /keys with the key roles each one grants. You can edit them, or add one from three fields: the role that may issue, the roles a key may carry and, optionally, its limits. They are ordinary permissions, also shown on the Permissions page, and like any ACL change they apply within 20 seconds.
Turn it on
Open API Keys and click Enable; the badge beside the title says whether the feature is on. It is free on every plan.
Disable turns keys off: POST /keys answers 403 whatever the ACL says, and no key authenticates. Nothing is deleted. Enable again and the keys, the permissions and the limits are as they were.
Below, the default limits:
| Setting | What it decides |
|---|---|
Longest a key may live |
The default, in days, up to ten years. A permission may set a lower one for its roles. A user may ask for less, never for more. |
Keys per user, at most |
The default. A permission may set its own. A user at the limit has to revoke one before issuing another. |
Issue a key
Your application calls the service, with the user’s own credentials:
curl -u alice@example.com:her-password \
-X POST https://c0ffee.ulabase.app/keys \
-H 'Content-Type: application/json' \
-d '{ "name": "my agent", "roles": ["cli-rw"], "expiresIn": 90 }'
{
"_id": "68c7…",
"name": "my agent",
"roles": ["cli-rw"],
"expiresAt": { "$date": 1765555200000 },
"key": "ulak_Kx7Rz9…"
}
The key is shown once. Only its SHA-256 hash is stored, so nobody can recover it afterwards, through any endpoint. An application that loses a key issues another.
A key’s roles must be among those named in the apiKeys.roles of the permission that authorised the call, whatever roles the user holds. With the permissions above, alice as an admin may ask for ["cli-rw"] or ["api-admin"], and bob as a user only for ["cli-ro"]. Without roles, the key has none and can do nothing.
The key always belongs to the user who signed in. No field names another user, and the rest of the body is ignored.
expiresIn is in days, defaulting to 90 or the maximum that applies, whichever is lower: the permission’s, when it sets one, otherwise the service’s.
Use a key
curl https://c0ffee.ulabase.app/things \
-H 'Authorization: Bearer ulak_Kx7Rz9…'
The request runs as the user who issued the key, with the key’s roles only. The ACL applies as it does to a password sign-in, readFilter and projectResponse included. An MCP client sends the same header to /mcp.
A key works only on the service that issued it, even if another of your services uses the same role names.
List and revoke
A user sees their own keys, without the key or its hash:
curl -u alice@example.com:her-password https://c0ffee.ulabase.app/keys
curl -u alice@example.com:her-password -X DELETE https://c0ffee.ulabase.app/keys/68c7…
lastUsedAt is absent until the key is first used, so you can tell a forgotten key from one in use.
via says how the key was issued: api with POST /keys, oauth when an agent signed the user in. client names what asked for it, such as claude.ai. Apart from these two fields, both kinds of key are the same.
The API Keys page shows every key issued on the service: who holds it, which client asked for it, and when it was last used. Revoke any with the bin icon; it takes effect within 20 seconds.
A key from a sign-in
An agent that signs the user in through the browser, such as Claude with your service added as a connector, gets its key by signing in. After the credentials, the login page asks the user which role the key gets and for how long, within the same apiKeys block that applies to POST /keys. Then /token hands the agent a ulak_ key.
There is nothing more to set up: enable keys, and grant /keys with the block to the roles that may issue. The user sees the key in GET /keys, and you on the API Keys page, with via: "oauth" and the client. Revoking it disconnects the agent.
The key is not renewed. When the time the user chose runs out, the agent asks them to sign in again. Every sign-in issues a new key, and the old ones stay until they expire or are revoked. A user at the limit is told on the login page to revoke one first.
You choose what the sign-in hands out on the MCP page, under OAuth login page: a key when the user may have one and the user’s own token otherwise (the default), a key or nothing, or always the user’s own token. See MCP, What the agent gets.
Try the MCP catalogue with a key
The Catalogue check panel on the MCP page accepts an API key: test with it. An agent’s catalogue depends on what its credential may read, and a key carries only the roles it was issued with. A test with the user’s password shows what the user sees, which may be more than the key.
Related pages
-
MCP: where an agent uses the key.
-
Tokens: the credential of your Ulabase account, for the console and
ulabase. -
Permissions: where the
apiKeysblock lives, and what a key’s roles may do.