Skip to main content

Grants & Client Access

The Apps contract in practice: resource grants that allow what an app may read or act on, per-key client grants, and pickable resource and client endpoints.

Apps access is two orthogonal grant families. Resource grants (/v1/apps/:id/grants) are the app's capability policy: what the *app itself* may read or act on. Client grants (/v1/apps/:id/clients) govern what one *API key* may do to the app. Effective power is always the intersection — token grant ∩ app capability policy — layered after tenant isolation and Sources-IAM. Resource grants are the platform's first allow-model primitive: absence of a row means the app cannot read the resource, whatever the calling token holds.

A resource grant names a resource_type (data_product, reference_table, source_connection, app, or action), a resource_id (a UUID for platform resources; the action name for type action), and an access level (read or act). Every input binding of a published version must be covered by a read grant, and every entry in acts by an act grant of type action — the input assembler checks grants live on every run, so revocation takes effect immediately, with no caching.

A client grant pairs an api_key_id with a grant_level: read (see the app, its runs and ledger), operate (run, dry-run, configure, resolve non-approval reviews, enable/disable), or decide (External mode: claim decision tasks — implying nothing else beyond the journaled disclosure of a claimed task's input package). Reading either list requires only the read tier, but every mutation is owner-only and human-only: machine callers get 403 Grant and client management requires a human workspace owner, whatever scopes they hold.

The first client grant on a key flips that key from legacy scope behavior to strict per-app enforcement everywhere on the Apps surface. Plan the transition per key: grant every app the key must keep touching in the same session as its first grant.
POST/v1/apps/:id/grants

Body

resource_type*stringdata_product | reference_table | source_connection | app | action.
resource_id*stringResource UUID, or the action name for resource_type "action". Max 256 chars.
access*stringread or act.
POST/v1/apps/:id/clients

Body

api_key_id*uuidThe workspace API key receiving the grant (pick via GET /v1/apps/pickable-clients).
grant_level*stringread | operate | decide. owner does not exist as a machine grant level.

curl — inspect access from either side

# What may this app read or do?
curl -s https://api.talonic.com/v1/apps/$APP_ID/grants \
  -H "Authorization: Bearer tlnc_your_api_key"
# → { "grants": [ { "id": "…", "resource_type": "data_product",
#      "resource_id": "1f6f2f57-…", "access": "read", "created_at": "…" } ] }

# Which keys may touch this app, at what level?
curl -s https://api.talonic.com/v1/apps/$APP_ID/clients \
  -H "Authorization: Bearer tlnc_your_api_key"
# → { "clients": [ { "id": "…", "api_key_id": "22e9f8d8-…",
#      "grant_level": "operate", "created_at": "…" } ] }

Response (403) — grant mutation from a machine caller

{
  "statusCode": 403,
  "code": "INSUFFICIENT_PERMISSIONS",
  "error": "forbidden",
  "message": "Grant and client management requires a human workspace owner.",
  "retryable": false,
  "request_id": "req_8a4e83d8fcfa4dc7",
  "path": "/v1/apps/80afca3e-c66c-4f07-abbf-78913ee80dff/grants"
}

Pickable resources and clients

Two helper endpoints keep grant forms free of pasted UUIDs. GET /v1/apps/pickable-resources?type=<resource_type> (read tier) lists grantable resources with human labels — { items: [{ id, label, sublabel }] }, e.g. a data product's name with its description, a reference table with its row count, an app with its slug; ?exclude=<uuid> omits one id (typically the app being configured, since an app never binds itself). GET /v1/apps/pickable-clients (owner tier, human only) lists the workspace's API keys as pickables for the client-grant form — it lives on the Apps surface rather than /v1/account/keys so the web session reaches it through the dual-auth head.

Revocation is symmetric and instant: DELETE /v1/apps/:id/grants/:grantId removes a capability, DELETE /v1/apps/:id/clients/:grantId removes a key's level — both owner-only, both answering { "revoked": true }, both effective on the next access check because grants are read live. Deleting an app or a key cascades its grant rows away.

Frequently asked questions

Why does my key with write scope get 403 on one app but not another?+
The key holds at least one client grant, so it is under strict per-app enforcement: it reaches exactly the apps it has grant rows for, at the granted level, and the legacy write scope no longer matters. Grant it the missing app or clear its grants to restore legacy scope behavior.
What is the decide grant level for?+
External mode. decide lets a key claim the app's decision tasks, submit or release or fail them, and raise reviews on its own claimed task — nothing else. It deliberately implies neither read nor operate, apart from the journaled disclosure of a claimed task's frozen input package.
Do resource grants filter what my token can read directly?+
No — they constrain the app. Resource grants are the app's capability policy, checked when the input assembler reads bindings and when the outbox accepts actions. Your token's own reach is governed by its scopes and client grants; effective power through an app is always the intersection of both.
Why is there no owner grant level for keys?+
Owner-tier operations — grant and client management, review-team writes, deletion, pickable-clients — are governance decisions the platform reserves for human workspace owners. Automating them would let a leaked key rewrite its own authority, so the guard rejects machine callers at that tier unconditionally.