---
title: "Install an external app (human workspace owner only)"
method: POST
path: "/v1/apps/{id}/install"
tags: ["Apps"]
---

# Install an external app (human workspace owner only)

`POST /v1/apps/{id}/install`

## Request

- Base URL: `https://api.talonic.com`
- URL: `https://api.talonic.com/v1/apps/{id}/install`
- Auth: HTTP bearer

Mints the service key carrying the `decide` grant plus the manifest's `required_grants`, stores the surface URL and allowed browser origin, and pre-consents the OAuth client. The plaintext key is returned ONCE and is never retrievable again. Machine callers are 403 whatever grants they hold.

## Path parameters

- `id` string, uuid, required

## Request body

- object
  - `surface_url` string: https URL of the external service UI; surfaces as endpoints.surface.
  - `allowed_origin` string: Exact https origin admitted for the surface's browser calls. Must match the host of one of the OAuth client's registered redirect URIs, so oauth_client_id is required with it.
  - `oauth_client_id` string, uuid
  - `key_name` string
  - `confirm_subscriptions_endpoint` string: The manifest's `subscriptions_endpoint`, retyped by the owner (§B13.2 check 2). Required, and compared byte-for-byte, whenever the active version subscribes to anything.
  - `bindings` object: Requirement slots bound to workspace resources, `{ "<alias>": "<uuid>" | "self" }` — one entry per non-optional, non-`self`-scope slot the manifest's `requires[]` declares (see `AppManifest.requires`).

## Response `201`

Installed. The service key AND the subscription signing secret appear here and nowhere else.

- object
  - `install` AppInstall: The binding between one workspace and one external service (Apps spec B7.1). Not versioned and never part of the manifest: it holds the ids that exist in one deployment only (service key, OAuth client, URL, origin).
    - `app_id` string, uuid
    - `status` 'installed' | 'suspended' | 'uninstalled'
    - `service_api_key_id` string, uuid, nullable: The workspace key the service authenticates with; null once revoked.
    - `oauth_client_id` string, uuid, nullable: The pre-consented OAuth public client the surface exchanges a session code for.
    - `surface_url` string, nullable
    - `allowed_origin` string, nullable: Exact https origin (scheme + host + optional port), no wildcard, no path.
    - `installed_by` string, nullable
    - `installed_at` string, date-time, nullable
    - `suspended_at` string, date-time, nullable
    - `suspended_reason` string, nullable: 'key_revoked' when the service key was revoked; otherwise the owner's reason.
    - `last_seen_at` string, date-time, nullable
    - `uninstalled_at` string, date-time, nullable
    - `approved_version` integer, nullable: The manifest version this install's grants, subscriptions and bindings were last synced against (§B13.2).
    - `approved_at` string, date-time, nullable
    - `approved_by` string, nullable
    - `pending_approval_version` integer, nullable: Set when a publish or hosted refresh parked a newer version on this install; while set the install is fail-closed (no embedded session, no run start, no decision-task offer, no delivery) until `POST .../install/approve`.
    - `subscriptions` AppSubscription[]
      - `id` string, uuid
      - `event_type` string
      - `match` object
      - `endpoint_url` string
      - `status` 'pending_verification' | 'active' | 'paused' | 'suspended' | 'revoked'
      - `failure_count` integer
      - `last_delivery_at` string, date-time, nullable
      - `verified_at` string, date-time, nullable
    - `bindings` AppInstallSlotBinding[]: What each requirement slot of the manifest was bound to at install.
      - `alias` string
      - `kind` string: AppResourceType of the bound resource.
      - `resource_id` string, uuid
      - `bound_at` string, date-time
  - `service_key` string
  - `masked_prefix` string
  - `subscription_secret` string, nullable: The signing secret every subscription row of the install shares; null when the manifest subscribes to nothing.
  - `subscriptions` AppInstallSubscriptionsState: What an install/approve/verify/rotate call reports about the install's subscription rows as a whole.
    - `status` 'pending_verification' | 'active' | 'paused' | 'suspended' | 'revoked', nullable: The status every row of the install carries, or null when it subscribes to nothing.
    - `verified` boolean
    - `reason` string: Machine reason from the endpoint-ownership challenge when verified is false.
    - `count` integer
  - `warning` string

## Other responses

- `400`: The bindings or the endpoint confirmation are incomplete; `message` starts with one of `endpoint_not_confirmed:` (confirm_subscriptions_endpoint missing or not byte-equal to the manifest's subscriptions_endpoint), `slot_unbound:` (a required, non-`self` slot has no binding), or `slot_unknown:` (a bindings alias the manifest does not declare).
- `403`: The API key does not have the required scope.
- `409`: The request conflicts with the current resource state.
- `422`: A required grant target, or a bound requirement-slot resource, does not exist here, or the origin is not admissible.

---

[API](https://skmtc.dev/talonic/apis/talonic-api.md) · [All operations](https://skmtc.dev/talonic/apis/talonic-api/llms.txt) · [OpenAPI document](https://skmtc.dev/talonic/apis/talonic-api/revisions/678fe5f8c9c3?raw)
