---
title: "Plan an install"
method: POST
path: "/v1/installs/plan"
tags: ["Installs"]
---

# Plan an install

`POST /v1/installs/plan`

## Request

- Base URL: `https://api.banksync.io`
- URL: `https://api.banksync.io/v1/installs/plan`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

What installing a template would do, without changing anything: each input it needs and whether `bindings` answers it (`bound`, `invalid`, `missing`), what will be created, an unverified listing’s install cap, `planCaps`: per kind (tables, feeds, enrichments) what the install needs against what the workspace plan has left, and `planGated`: the steps the workspace plan does not include (an AI categoriser on a lower plan), which the install skips — never refuses — with “Included on <Plan>: …” (`planRequirements` lists every one, whatever the plan). Answer every `missing` or `invalid` input, then `POST /installs` with the same body.

## Headers

- `Idempotency-Key` string: Makes a retry safe: the same key with the same request replays the first answer for 24 hours instead of running twice, and the same key with a different request is a 409 `IDEMPOTENCY_KEY_REUSED`. Refusals a retry can outlive — 408, 409, 423, 425, 429 and every 5xx — are never replayed, so retrying with the same key after the cause clears runs the request.

## Request body

- object
  - `source` 'listing' | 'workspace' | 'preset' | 'inline', required: `listing` (with `listingId`), `workspace` (a saved blueprint, `blueprintId`), `preset` (`presetKey`) or `inline` (`blueprint`)
  - `listingId` string: From `GET /marketplace/listings`
  - `releaseId` string: A specific release of the listing; default its current approved release
  - `linkToken` string: The private-link token (the `?link=` value of the private link) of a listing that is not publicly listed; required to install one
  - `blueprintId` string
  - `presetKey` string
  - `blueprint` object: A portable blueprint
  - `bindings` object: Slot id → binding: `{"kind":"bank","bankId"}`, `{"kind":"feed","feedId"}`, `{"kind":"connection","integrationId"}`, `{"kind":"container","destination"}`, `{"kind":"resource","id"}` or `{"kind":"value","value"}`. The plan lists each slot and what answers it.
  - `resourceKeys` string[]: Install only these resources (by key) and what they need

## Response `200`

The plan

- object
  - `success` true, required
  - `data` object, required
    - `ok` boolean, required: True when every required input is answered and fits
    - `source` object, required
      - `source` string, required
      - `listingId` string, nullable, required
      - `releaseId` string, nullable, required
      - `blueprintId` string, nullable, required
      - `blueprintKey` string, nullable, required
    - `blueprint` object, required
      - `title` string, required
      - `description` string, nullable, required
      - `limitations` string[], required
      - `resourceCount` integer, required
      - `kinds` string[], required
    - `inputs` object[], required
      - `slot` string, required
      - `kind` string, required
      - `label` string, required
      - `optional` boolean, required
      - `status` 'bound' | 'invalid' | 'missing' | 'unbound' | 'unused', required
      - `usedBy` string[], required
    - `resources` object[], required
      - `key` string, required
      - `kind` string, required
      - `name` string, required
      - `action` 'create' | 'skip' | 'out-of-scope', required
      - `reason` string, nullable, required
      - `dependsOn` string[], required
    - `issues` object[], required
      - `code` string, required
      - `message` string, required
      - `slot` string, nullable, required
      - `resourceKey` string, nullable, required
    - `suggestedName` string, required
    - `automationsNeedEnable` boolean, required
    - `outboundEffects` object[], required
      - `resourceKey` string, required
      - `kind` string, required
      - `name` string, required
      - `effect` 'alerts' | 'outbound-step', required
      - `detail` string, required
    - `cap` object, nullable, required
      - `capped` boolean, required
      - `limit` integer, required
      - `used` integer, required
    - `planCaps` object, required
      - `ok` boolean, required
      - `shortfalls` object[], required
        - `kind` 'feed' | 'enrichment' | 'table', required
        - `reason` 'count' | 'tier', required
        - `needed` integer, required
        - `remaining` integer, nullable, required
        - `limit` integer, nullable, required
        - `used` integer, nullable, required
        - `resourceKeys` string[], required
        - `optionalResourceKeys` string[], required
        - `lockedTypes` string[], nullable, required
        - `message` string, required
    - `planGated` object[], required
      - `resourceKey` string, required
      - `kind` string, required
      - `name` string, required
      - `plan` string, required: The lowest plan that includes it (`professional`)
      - `planLabel` string, required
      - `adds` string, required: What it adds, in words ("AI categorising")
      - `reason` string, required: "Included on Professional: AI categorising"
      - `requirement` 'count': `count`: what the template makes needs more of a capped thing (tables, a bank connection) than lower plans allow, so it needs this plan to install. Absent: a plan-gated step, skipped below its plan.
    - `planRequirements` object[], required
      - `resourceKey` string, required
      - `kind` string, required
      - `name` string, required
      - `plan` string, required: The lowest plan that includes it (`professional`)
      - `planLabel` string, required
      - `adds` string, required: What it adds, in words ("AI categorising")
      - `reason` string, required: "Included on Professional: AI categorising"
      - `requirement` 'count': `count`: what the template makes needs more of a capped thing (tables, a bank connection) than lower plans allow, so it needs this plan to install. Absent: a plan-gated step, skipped below its plan.
    - `shared` object[], required
      - `resourceKey` string, required
      - `kind` 'table' | 'feed' | 'enrichment', required
      - `sharedKey` string, required
      - `resourceId` string, required
      - `name` string, required: The existing resource’s name
      - `fromInstall` object, required
        - `id` string, required
        - `name` string, required
      - `message` string, required: "Uses your existing Transactions table (from Money Map)"

## Other responses

- `400`: Invalid input — `code` names it: `VALIDATION_FAILED`, `INVALID`, `INVALID_BLUEPRINT` (not a blueprint this workspace can read), `PORTABILITY_VIOLATION` (it still carries workspace ids), `UNSAFE_BLUEPRINT`, `INVALID_BINDINGS` (an input is missing or does not fit), `RESTORE_WINDOW_PASSED`; `details[]` names each field
- `401`: Unauthorized
- `403`: Forbidden — a missing `marketplace:read` / `marketplace:write` scope, a role below editor for a change, an installed app outside the public marketplace, an unverified listing already installed in ten workspaces (`INSTALL_CAP_REACHED`), or a template that needs more tables, feeds or enrichments than the workspace plan has left (`PLAN_LIMIT_REACHED`)
- `404`: Not found — something this workspace may not see answers exactly as something that does not exist; or the marketplace is not enabled for this workspace
- `409`: Conflict — it changed since you read it (`STALE`), it is busy or in the wrong state (`INVALID_STATE`), the listing cannot be installed (`NOT_INSTALLABLE`), a key is taken (`KEY_TAKEN`), or an existing container does not fit (`ADOPTION_CONFLICT`)
- `429`: Too many — the key’s rate limit, or a marketplace quota (`QUOTA_EXCEEDED`: 200 saved templates per workspace, 50 installs per hour). `Retry-After` says when to come back.
- `503`: The marketplace is temporarily unavailable; retry shortly

---

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