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, a resource_id, and an access level (read, act, or write). The types are data_product, reference_table, source_connection, app, and action — the action name stands in for the id — plus spec and pipeline for apps that work with Specs and their runs, and field_registry for read access to the organization's field registry (names, meanings, synonyms). 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; write is meaningful only on a promotion target such as a reference table. Grants are checked live on every run, so revocation takes effect immediately, with no caching.
A grant may cover a whole type instead of one resource: resource_id: "*" means every resource of that type in the organization, and is accepted for data_product, reference_table, spec, pipeline, and field_registry (a * on any other type is a 422). The field registry is one resource per organization, so a field_registry grant is always * and always read. Access levels are matched exactly, never as a ladder: a write row does not cover a read check, so an app that both reads and acts on something holds two rows.
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: work the app's decision tasks — implying nothing else beyond the journaled disclosure of a claimed task's input package). An OAuth session reaches the same decide authority through the apps:decide scope, bounded by the person's live Senior Member or higher role. 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.
/v1/apps/:id/grantsBody
/v1/apps/:id/clientsBody
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"
}What an installed app's service key reaches
An installed app works through its own service key, and that key reaches nothing its approved grants do not cover. On the Apps surface it is scoped to its own app, runs, and reviews. On the shared /v1 surfaces a grant is what opens the door: a spec grant at read lets the key list and read the validation review queue (GET /v1/review, filtered to granted Specs — no grant means an empty page) and the field-review queue, and the same grant at act lets it action items (POST /v1/review/:id/action, POST /v1/review/batch, and the field-review resolve routes), with any ungranted id in a batch reported as insufficient_grant and left untouched. Record sets, pipelines, and documents open the same way under spec, data_product, pipeline, and document-source grants. Aggregate views — review stats, assignment, triage, value clusters, and decision logs — stay closed to service keys, because a workspace-wide count cannot be narrowed to granted Specs.
Grant a whole type, and the field registry
# Read every data product in the organization
curl -s -X POST https://api.talonic.com/v1/apps/$APP_ID/grants \
-H "Authorization: Bearer $OWNER_SESSION_TOKEN" -H "Content-Type: application/json" \
-d '{"resource_type": "data_product", "resource_id": "*", "access": "read"}'
# Read the field registry (always "*", always read)
curl -s -X POST https://api.talonic.com/v1/apps/$APP_ID/grants \
-H "Authorization: Bearer $OWNER_SESSION_TOKEN" -H "Content-Type: application/json" \
-d '{"resource_type": "field_registry", "resource_id": "*", "access": "read"}'
# A * on a type that names one resource is refused
# → 422 "a source_connection grant names one resource; '*' is allowed for review, pipeline, data_product, reference_table, spec, field_registry"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.