Authentication & Access
API applications & keys
Create a service account, assign its roles, then mint an API key that runs as that account.
API applications are tenant-scoped server-to-server clients. Each application holds one API key and runs as one service account. The key can do whatever that service account's roles allow.
Keep API keys on the server. Browser embeds should use public surfaces or authenticated user sessions, not API application keys.
Create a service account
Open Settings → Identity & access → Service accounts and create an account. Open the account and assign roles. Roles use the same namespace:action rules as a person's access. An API call succeeds only when one of those rules matches the request.
The account must be an active service account in the same workspace. People, customers, and AI agents cannot own an API app.
To read or write one Object, grant that Object's namespace. A National Asset Register stored as objects.NationalAssetRegister needs objects.NationalAssetRegister:retrieve to list records, and the matching action for create, update, or delete.
Create an app
Open Settings → Application → API, or call createApp on system.App. user is the service account id. The key is returned once. List views never include the raw token.
1{
2 "__meta__": {
3 "namespace": "system.App",
4 "schema": "model",
5 "intent": "consequence",
6 "consequence": "createApp",
7 "sideEffects": { "consequenceType": "class" }
8 },
9 "name": "National Asset Register sync",
10 "user": 12,
11 "type": "persistent",
12 "description": "Reads asset records for an external register"
13}The payload returns { "id": …, "token": "jql:kp-…::…" }. Store the token immediately. type is persistent or ephemeral. expiry is optional. Leave it unset for a key that does not expire.
Send the key
Keys look like jql:kp-<workspace-alias>::<secret> (kp = persistent, ke = ephemeral). Two equivalent ways. The header is preferred. In curl, wrap the header in single quotes, or put the token in $ANTLY_JQL_TOKEN, so the shell cannot expand $ or globs inside the secret.
Header (forces authenticationClass: "api_key" even if __meta__ omits it):
POST /jql HTTP/1.1
Authorization: Bearer jql:kp-acme::…
Content-Type: application/jsonX-Antly-Api-Key is accepted as an alternative to Authorization.
Body
1{
2 "__meta__": {
3 "namespace": "objects.NationalAssetRegister",
4 "schema": "model",
5 "intent": "retrieve",
6 "authenticationClass": "api_key",
7 "authenticationToken": "jql:kp-acme::…",
8 "limit": 25,
9 "return": {
10 "id": null,
11 "reference": null
12 }
13 }
14}If both header and body tokens are present and differ, the request is rejected with 401 API_KEY_MISMATCH.
What the key is allowed to do
Antly authenticates the key, loads the API app, and then checks permissions as that service account. There is no separate scope list on the app.
Two checks apply:
- Service account roles. The account's roles, teams, and direct permission rules are evaluated exactly as they would be for that account signed in. No matching rule means
403 PERMISSION_DENIED. - API key ceiling. A key may only call
profiles.*,objects.*,tickets.*,tasks.*, andworkflow.*. A role that allows something outside that ceiling still cannot be used with an API key. That refusal is403 API_KEY_NAMESPACE_NOT_EXPOSED.
Role rules use namespace:action. There is no model: or controller: prefix.
| Rule | Meaning |
|---|---|
objects.NationalAssetRegister:retrieve | List records on that Object |
objects.NationalAssetRegister:retrieveOne | Read one record |
objects.NationalAssetRegister:create | Create records |
objects.NationalAssetRegister:consequence.archive | Call only the archive consequence |
objects.NationalAssetRegister:* | Any intent on that Object |
tickets.Ticket:retrieve | List tickets |
tickets.*:* | Everything in the tickets module |
Actions are matched after camelCase → snake_case normalisation, so retrieveOne and retrieve_one are the same rule. Prefer exact rules. * covers CRUD, consequences, and controller methods on that namespace.
Record filters on a role (filter, exclude, or q) still apply. If a filter hides a row, retrieveOne returns 404, not 403.
PostgreSQL Objects use schema: "model" and the Object namespace. DynamoDB Objects use schema: "nosql" and the same namespace.
Errors
| HTTP | errorCode | When |
|---|---|---|
| 401 | API_KEY_MISSING | api_key class with no token in header or body |
| 401 | API_KEY_INVALID | Token unknown, inactive, expired, or not attached to an app |
| 401 | API_KEY_MISMATCH | Header and body tokens differ |
| 403 | API_APP_DISABLED | The app is disabled, or it has no active service account |
| 403 | PERMISSION_DENIED | The service account's roles do not allow this namespace:action |
| 403 | API_KEY_NAMESPACE_NOT_EXPOSED | Outside the API key ceiling |
Token lifecycle
Tokens are shown in full only at create and rotate. Validation updates last_used. Ephemeral tokens are deactivated after use.
Every API-key request is written to api.JqlLog with authenticationClass = "api_key" and apiAppId set. Administrators see this on the app Activity tab (filter: { apiAppId }, newest first).
Rotate by calling rotateToken on the app, deploying the new secret, then discarding the old key. Disable or enable the app without rotating when you need to pause traffic. Deleting the app invalidates the key. revealToken is audited.
For the JQL request contract, read /reference/jql-overview. For namespace naming and dynamic object namespaces, read /reference/namespaces. For error envelopes, read /reference/errors.