Authentication & Access
API applications & keys
Create a system.App, send its JQL token, and grant namespace:action directives with optional record filters.
API applications are tenant-scoped server-to-server clients. A workspace administrator creates one under Settings → Application → API. Creating a system.App mints a linked JqlToken; your service sends that token as an API key.
Keep JQL tokens on the server. Browser embeds should use public surfaces or authenticated user sessions, not API application keys.
The workspace console is Settings → Application → API. Create an app there (name, run-as user, optional expiry). The key is shown once. Open the app to assign scopes, reveal or rotate the key, and inspect Activity (api.JqlLog filtered by apiAppId). List views never return the raw token.
Create an app
1{
2 "__meta__": {
3 "namespace": "system.App",
4 "schema": "model",
5 "intent": "consequence",
6 "consequence": "createApp",
7 "sideEffects": { "consequenceType": "class" }
8 },
9 "name": "CRM sync",
10 "user": 12,
11 "type": "persistent",
12 "description": "Server-to-server CRM client"
13}The payload returns { "id": …, "token": "jql:kp-…::…" }. Store the token immediately.
Assign scopes
Save scopes as a whole with replaceAll on system.AppNamespaceConfiguration. Each item is a namespace:action directive plus optional q / filter / exclude.
1{
2 "__meta__": {
3 "namespace": "system.AppNamespaceConfiguration",
4 "schema": "model",
5 "intent": "consequence",
6 "consequence": "replaceAll",
7 "sideEffects": { "consequenceType": "class" }
8 },
9 "app": 8,
10 "directives": [
11 {
12 "directive": "tickets.Ticket:retrieve",
13 "q": { "status": { "$eq": "open" } },
14 "active": true
15 },
16 {
17 "directive": "tickets.Ticket:retrieveOne",
18 "q": { "status": { "$eq": "open" } },
19 "active": true
20 }
21 ]
22}Send the key
Keys look like jql:kp-<workspace-alias>::<85 alphanumeric characters> (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": "tickets.Ticket",
4 "schema": "model",
5 "intent": "retrieve",
6 "authenticationClass": "api_key",
7 "authenticationToken": "jql:kp-acme::…",
8 "limit": 25
9 },
10 "id": null,
11 "status": null
12}If both header and body tokens are present and differ, the request is rejected with 401 API_KEY_MISMATCH.
What an app is allowed to do
Access is the intersection of three layers:
- Platform ceiling — the namespaces Antly will expose to any API key:
profiles.*,objects.*,tickets.*,tasks.*, andworkflow.*. - App directives — one or more
namespace:actionrules on the app. Deny by default: an app with no matching directive is refused. - Run-as user RBAC — the app acts as a named workspace user. That user's roles apply exactly as if they were logged in.
Directives use the same grammar as workspace permissions. There is no model: or controller: prefix.
| Directive | Meaning |
|---|---|
tickets.Ticket:retrieve | List tickets |
tickets.Ticket:retrieveOne | Read a single ticket |
tickets.Ticket:create | Create tickets |
tickets.Ticket:consequence.archive | Call only the archive consequence |
tickets.Ticket:consequence | Call any consequence on tickets |
tickets.Ticket:getMyBadgeCount | One controller method |
tickets.Ticket:* | Any intent on tickets |
objects.Customer:* | Any intent on the dynamic object Customer |
tickets.*:* | Everything in the tickets module |
Actions are matched after camelCase → snake_case normalisation, so retrieveOne and retrieve_one are the same directive. Prefer exact directives; * is broader than it looks (CRUD, consequences, and controller methods on that namespace).
A directive may carry a record filter (filter / exclude / q) that is ANDed onto every query under that directive. If the filter excludes a row, retrieveOne returns 404, not 403.
Example app-scoped retrieve
1{
2 "__meta__": {
3 "schema": "model",
4 "namespace": "objects.Record",
5 "intent": "retrieve",
6 "authenticationClass": "api_key",
7 "return": {
8 "id": null,
9 "data": null,
10 "modified": null
11 }
12 },
13 "filter": {
14 "object__namespace": "Invoice"
15 }
16}If the app directive also has q: { "data.status": { "$eq": "approved" } }, the token can only see approved invoice records even if the request asks for more.
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 | An administrator has disabled the app |
| 403 | API_APP_SCOPE_DENIED | No directive on the app permits this namespace:action |
| 403 | PERMISSION_DENIED | Directive allowed it but the run-as user does not |
| 403 | API_KEY_NAMESPACE_NOT_EXPOSED | Outside the platform 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 / 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.