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

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 | spec | pipeline | field_registry.
resource_id*stringResource UUID, the action name for resource_type "action", or "*" for a whole-type grant (data_product, reference_table, spec, pipeline; always "*" for field_registry). Max 256 chars.
access*stringread, act, or write. write applies only to promotion targets; field_registry accepts read only. A duplicate (type, id, access) row is a 409.
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"
}

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.

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.
When should I use a whole-type grant?+
When an app genuinely works across every resource of a type — for example a monitoring app that reads all data products. A "*" grant follows new resources automatically, so it is broader than it looks; prefer per-resource grants for apps that read a known set of inputs.
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.