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

json
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.

json
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):

http
POST /jql HTTP/1.1 Authorization: Bearer jql:kp-acme::… Content-Type: application/json

X-Antly-Api-Key is accepted as an alternative to Authorization.

Body

json
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:

  1. Platform ceiling — the namespaces Antly will expose to any API key: profiles.*, objects.*, tickets.*, tasks.*, and workflow.*.
  2. App directives — one or more namespace:action rules on the app. Deny by default: an app with no matching directive is refused.
  3. 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.

DirectiveMeaning
tickets.Ticket:retrieveList tickets
tickets.Ticket:retrieveOneRead a single ticket
tickets.Ticket:createCreate tickets
tickets.Ticket:consequence.archiveCall only the archive consequence
tickets.Ticket:consequenceCall any consequence on tickets
tickets.Ticket:getMyBadgeCountOne 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

json
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

HTTPerrorCodeWhen
401API_KEY_MISSINGapi_key class with no token in header or body
401API_KEY_INVALIDToken unknown, inactive, expired, or not attached to an app
401API_KEY_MISMATCHHeader and body tokens differ
403API_APP_DISABLEDAn administrator has disabled the app
403API_APP_SCOPE_DENIEDNo directive on the app permits this namespace:action
403PERMISSION_DENIEDDirective allowed it but the run-as user does not
403API_KEY_NAMESPACE_NOT_EXPOSEDOutside 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.