Apps Overview
The Talonic Apps surface: named, versioned decision apps over governed data, with an ETag-cached catalog, dual auth heads, and a read/run/operate/configure ladder.
Apps are named, versioned, publishable decision units. An app reads a governed set of inputs — published data products, reference tables, connected databases, or other apps — and produces schema-typed decisions with a replayable, provenance-carrying ledger: an append-only run journal, a queryable run projection, and an immutable sealed decision record per run. Every app runs in one of three autonomy modes: rules (deterministic policy cards decide), assisted (rules decide, a draft-only agent proposes review resolutions), or external (an outside agent decides inside Talonic's contract and ledger).
The REST surface spans these route families: /v1/apps (catalog, lifecycle, logic, versions, acceptance, presentation and pinning, runs, standing decisions, reviews, rule candidates, precedents, grants, clients, plus hosted-app registration and installs and embedded surface sessions), /v1/apps/manifest/validate (public manifest validation, no credential), /v1/runs (run projection, verdict matrix, summary, journal, sealed record), /v1/replays, /v1/reviews (the Human Review inbox), /v1/rule-candidates, /v1/review-teams, and /v1/decision-tasks (the External-mode protocol). The Talonic web UI is a client of exactly these endpoints — it holds no private capabilities — and every enabled app is additionally published as a dynamic MCP tool named in its manifest's endpoints.mcp_tool (app_<slug>, with - replaced by _), so an external agent given only a token discovers and operates the same surface a person sees.
Authentication is dual-headed: tlnc_ API keys and OAuth tokens on one head, platform session JWTs on the other, both landing on the same authorization layer. Authorization is a four-tier ladder — read < run < operate < configure (= owner) — mapped for humans from the workspace role (viewer → read, member → run, Senior Member (senior_member) → operate — resolving reviews, replays, and the disable kill switch, owner → configure — creating, editing drafts, publishing, enabling, and promoting rule candidates). Machine callers are decided by per-app grants: a read grant admits reads, an operate grant admits read/run/operate and the configure-tier actions, and true owner-tier routes (grant management, deletion, pickable clients) always require a human workspace owner — they are never machine-grantable. An OAuth token issued to a person carries that person's workspace role, and the tier it reaches is the lower of what its scopes admit and what the role admits: a token can never outrank the person it was issued to.
A compatibility rule keeps pre-Apps keys working: a key with zero app-grant rows falls back to its legacy scopes — read scope for read-tier routes, write for run/operate. The moment the first grant row lands on a key, that key switches to strict per-app enforcement: it can only touch apps it holds grants for, at the granted level. OAuth clients and routes without an app id in the path (the workspace review inbox, /v1/runs/:runId) always use the scope path, with every read tenant-scoped by the service. On that path an OAuth client reaches read with apps:read and run/operate with apps:operate; the configure tier stays reachable only through legacy write.
The whole surface requires a concrete workspace. A key or session resolving to the cross-tenant master view is rejected with 403 and the message A concrete workspace is required for the Apps surface — there are no cross-tenant app reads or writes on /v1.
/v1/appsQuery parameters & headers
The response carries an etag both as an ETag header and in the body: a hash across each visible app's id, active content hash, enabled flag, pin state, display name, icon, and installed surface URL. It changes when an app is created, deleted, enabled, disabled, republished, renamed, re-marked, pinned or unpinned, or installed — which makes it the cache key for tool catalogs. MCP clients re-list tools when it moves; your integration can poll GET /v1/apps with If-None-Match cheaply and rebuild only on 200.
curl
curl -s https://api.talonic.com/v1/apps \
-H "Authorization: Bearer tlnc_your_api_key"
# Conditional re-fetch: 304 when nothing changed
curl -s -o /dev/null -w "%{http_code}" https://api.talonic.com/v1/apps \
-H "Authorization: Bearer tlnc_your_api_key" \
-H 'If-None-Match: "e3b0c44298fc1c149afbf4c8996fb924"'Response (200)
{
"apps": [
{
"app_id": "app_8f591d2d85c5990a",
"id": "80afca3e-c66c-4f07-abbf-78913ee80dff",
"slug": "invoice-approval",
"display_name": "Invoice approval",
"description": "Holds supplier invoices that drift from their purchase order.",
"version": 1,
"content_hash": "9df5a562cff6d2dfda738f4bf8f88b0b...",
"mode": "rules",
"enabled": true,
"display_icon": null,
"nav_pinned": true,
"nav_pinned_at": "2026-09-02T08:10:00.000Z",
"trigger_floor_at": null,
"created_at": "2026-08-29T11:31:36.698Z",
"updated_at": "2026-08-29T11:32:32.724Z",
"contract": { "reads": [], "acts": [] },
"output_contract": {
"type": "object",
"required": ["decision"],
"properties": { "decision": { "enum": ["approve", "hold"] } }
},
"input_schema": {
"type": "object",
"properties": {
"input": { "type": "object" },
"idempotency_key": { "type": "string", "maxLength": 64 },
"dry_run": { "type": "boolean" }
}
},
"triggers": { "api": true, "manual": true },
"thresholds": { "auto_execute_above_confidence": 0.9 },
"fallback": "hold",
"display": null,
"endpoints": {
"mcp_tool": "app_invoice_approval",
"rest": "/v1/apps/80afca3e-c66c-4f07-abbf-78913ee80dff"
},
"surface": { "embed": "link", "scopes": ["apps:read"], "context": [], "url": null },
"required_grants": [],
"subscriptions": [],
"subscriptions_endpoint": null,
"source": "pushed",
"manifest_url": null,
"identity": null,
"requires": [],
"allowed_origins": []
}
],
"etag": "\"e51b28d3552a4f39a4c88b1e02e7a3d1\""
}/v1/apps/:idThe decide grant
Beside the ladder sits decide, the only authority on the decision-task protocol that External-mode apps use. It is an independent grant, not a rung: operate does not imply it, it implies nothing else, and no legacy scope reaches it. Human web sessions are refused outright — the protocol is a machine surface. A tlnc_ key needs an explicit per-app decide client grant. An OAuth session needs the apps:decide scope, which is never pre-consentable (a person consents to it in person, every time) and is checked against the person's live workspace membership on every request: the membership must be active and at Senior Member or above, so removing or demoting someone takes effect immediately rather than when their token expires.
Refusal reasons
Every refusal an integration must branch on carries a stable top-level reason beside code in the error envelope. Branch on reason, never on error or the message text: code only names the HTTP family, and messages are prose. On the Apps surface the ladder refusals read unauthenticated, workspace_required, insufficient_tier (with details.required naming the tier), owner_required, insufficient_app_grant, insufficient_scope (with details.required_scopes), and decide_grant_required; install and hosted-manifest refusals add reasons such as install_pending_approval, install_not_ready, slot_unbound, and hosted_manifest_read_only. An app's own service key always receives insufficient_grant for anything its approved grants do not cover, so it branches once for "the owner must grant this".
Response (403) — a refusal with its reason
{
"statusCode": 403,
"code": "INSUFFICIENT_PERMISSIONS",
"reason": "insufficient_tier",
"error": "forbidden",
"message": "This action requires the 'configure' tier; your role is 'member'.",
"retryable": false,
"details": { "required": "configure" },
"request_id": "req_5c1e0b7a9f2d4e18",
"path": "/v1/apps/80afca3e-c66c-4f07-abbf-78913ee80dff/versions/3/publish"
}Webhook events
Apps emit workspace webhooks through the standard HMAC-signed delivery pipeline: app.run.completed and app.run.failed when a triggered run reaches a terminal state, app.review.raised and app.review.resolved around Human Reviews, app.version.activated when a publish lands, app.action.execute when a decision executes an action (the payload carries the decision, its evidence locators, rationale, and the action's idempotency key), and app.exception.raised for system-raised exceptions. Subscribe via POST /v1/webhooks exactly as for document events.
app.decision_task.offered— an External-mode run parked a decision task; carriestask_id,run_id,app,version,subject_count, anddry_runon a trial run. Also published to Delivery in the same transaction as the task row.app.run.sealed— a run minted its sealed decision record; carriesrun_id,app,version,status, plusrecord_set_idandsubjectswhen the record names them. Delivery only: subscribe through a delivery binding, not/v1/webhooks. Dry runs never emit it.app.install.suspended/app.install.resumed— an installed app stopped serving (suspended by the workspace or its service key revoked) or is serving again.app.version_fetched— a hosted app's manifest URL served a changed document, stored as afetchedcandidate; nothing was activated. Carriesapp_id,app,version,content_hash.app.subscription.suspended— an installed app's event subscription was suspended after consecutive delivery failures to its endpoint.
Frequently asked questions
What is the difference between the read, run, operate, and configure tiers?+
acts needs a human workspace owner regardless of tier. decide is separate from all four. True owner-tier routes — grant and client management, review-team writes, and app deletion — are always a human workspace owner, never an API key.