> ## Documentation Index
> Fetch the complete documentation index at: https://vetta.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Apps

> Hosted web applications: provisioning, deployments, secrets, custom domains, the app database, and the app's own MCP endpoint.

An **app** is a hosted web application the platform runs for your organization — a static or client-rendered frontend, optionally paired with a managed Postgres backend (`fullstack`). Apps are **org-level** resources, like [domains](https://vetta.sh/docs/api/domains.md): they do not belong to an agent, they outlive any session, and by default every agent in the organization can operate every app (narrow that per-agent with the `apps` tool config's `allowed_apps`).

Everything asynchronous — the fullstack backend coming up, a deployment building — advances on read: poll `GET /v1/apps/{id}` or the deployments list until the status is terminal.

## The app object

**id** `string`

Unique id (e.g. `app_01H...`).

**object** `string`

Always `app`.

**name** `string`

A lowercase DNS label, unique in the organization.

**description** `string | null`

Free text.

**type** `string`

`frontend_only` or `fullstack`.

**status** `string`

`provisioning`, `active`, or `error`. A `frontend_only` app is `active` at birth; a `fullstack` app is `provisioning` until its database is ready.

**url** `string | null`

The app's live URL.

**mcp** `string | null`

The path of the MCP endpoint the app itself serves (e.g. `/mcp`), or `null` when it serves none. `fullstack` only. See [App MCP tools](#app-mcp-tools).

**error** `string | null`

Set when `status` is `error`.

**created\_at** `string`

Creation timestamp.

## Create an app

`POST /v1/apps` — scope `agents:write`

Idempotent on `name`: creating an app that already exists returns the existing record.

**name** `string` *required*

A lowercase DNS label, 63 characters or fewer.

**description** `string`

Optional, up to 500 characters.

**type** `string`

`frontend_only` (default) or `fullstack`. A `fullstack` app gets a managed database; its connection string is pushed into the app's environment automatically when the database is ready.

**mcp** `string`

Optional. The path the app serves an MCP endpoint on — absolute, beginning with `/`, up to 128 characters. `fullstack` only: on a `frontend_only` app it is refused with `400 validation_failed` (param `mcp`), because there is no server to answer on it. See [App MCP tools](#app-mcp-tools).

```bash cURL theme={"system"}
curl -fsSL https://api.vetta.sh/v1/apps \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "name": "storefront", "type": "fullstack", "mcp": "/mcp" }'
```

```typescript TypeScript theme={"system"}
const app = await vetta.apps.create({ name: "storefront", type: "fullstack", mcp: "/mcp" });
console.log(app.mcp); // "/mcp"
```

## List, read, update, delete

* `GET /v1/apps` — scope `agents:read`. Paginated with `limit` and `after`.
* `GET /v1/apps/{id}` — scope `agents:read`.
* `PATCH /v1/apps/{id}` — scope `agents:write`. Mutable fields: `description` and `mcp` (see [App MCP tools](#app-mcp-tools)); the name and type are fixed at creation.
* `DELETE /v1/apps/{id}` — scope `agents:write`. Tears down the hosting project and, for `fullstack`, the app database with it. Irreversible.

## Deployments

A deployment ships the whole site in one request: a map of file paths to base64-encoded contents. The build runs asynchronously; the deployment advances `queued → building → ready` (or `error`) on read.

* `POST /v1/apps/{id}/deployments` — scope `agents:write`
* `GET /v1/apps/{id}/deployments` — scope `agents:read`

**files** `object`

Path → base64 contents, e.g. `{ "index.html": "PGgxPuKAplPC9oMT4=" }`. At least one file. Required unless `from_deployment` is given.

**content\_hash** `string`

An opaque digest of `files` you compute (up to 128 characters). Stored as sent and echoed on every read, so a client can tell an unchanged site from a changed one by comparing against the newest deployment before uploading again. Never computed or checked by the platform. Not allowed with `from_deployment`.

**from\_deployment** `string`

Instead of `files`: the id of one of this app's earlier deployments to ship again. The platform reads that deployment's [kept files](#deployment-files) and deploys them as a new deployment, with the same limits and checks as any deploy and that deployment's `content_hash`. Nothing is uploaded, so this is how to restore a site of any size. Answers `not_found` for an id that is not one of this app's deployments, and for one made before files were kept. Give exactly one of `files` or `from_deployment`.

**id** `string`

Deployment id (e.g. `apd_01H...`).

**object** `string`

Always `app_deployment`.

**status** `string`

`queued`, `building`, `ready`, or `error`.

**url** `string | null`

The deployment's URL once ready.

**error** `string | null`

Set when the build failed.

**content\_hash** `string | null`

The `content_hash` the deploy was created with; `null` when none was sent.

**created\_at** `string`

Creation timestamp.

### Deployment files

The deployment is the app's codebase: the platform keeps the `files` a deploy posted and gives them back as posted, so the newest deployment can be pulled, edited and shipped again without a repository in between.

An `AGENTS.md` at the root of the files is the app's notes for the agents that work on it: what it is, its files, its data and how to change it. It is kept with the codebase and never served. An agent's `pull` prints it, and its `deploy` refuses files that drop the one the app had.

* `GET /v1/apps/{id}/deployments/{apd}/files` — scope `agents:read`

**object** `string`

Always `app_deployment_files`.

**deployment\_id** `string`

The deployment the files belong to.

**files** `object`

Path → base64 contents, exactly as they were deployed.

Answers `not_found` for a deployment made before the platform kept deployment files, and for any id that is not one of this app's deployments.

## Secrets

App secrets are **write-only** environment variables: the value goes to the app's runtime and never comes back on any read — the list returns names and timestamps only. Setting an existing name overwrites it.

* `POST /v1/apps/{id}/secrets` — scope `agents:write`. Body: `name` (an environment variable name: `A-Z`, `0-9`, `_`) and `value`.
* `GET /v1/apps/{id}/secrets` — scope `agents:read`. Names and `created_at` only, never values.
* `DELETE /v1/apps/{id}/secrets/{name}` — scope `agents:write`.

## Opening an app's own dashboard

An app that gates itself with an operator bearer named `DASHBOARD_TOKEN` can be opened without anyone handling that token. The platform holds its own copy — it is what generated it — and mints a short-lived proof of it on request.

`POST /v1/apps/{id}/entry` — scope `agents:write`

```json theme={"system"}
{ "object": "app_entry", "url": "https://your-app.example/api/enter", "ticket": "1790000120000.Qm9v….eyJzdWIiOi….f3Xq…" }
```

`ticket` says who is opening the app — `{exp, sub, email, role, org, app}` — signed with an HMAC keyed by the app's token, good for two minutes. Treat the string as opaque and post it whole. It is **not** the token, and the token is still returned by no route. An app reads it with `@usenaive-sdk/app`'s `enter`, which checks the signature, the expiry and that the ticket names this app.

**Post the ticket; never link it.** Submit it as a form field to `url` from the operator's browser — the app trades it for its own session cookie and redirects to its home page. Putting it in a query string would leave it in the app's server logs, the browser's history and every `Referer` the page sends afterwards.

Refused with `feature_not_configured` on an app holding no `DASHBOARD_TOKEN`, and with `job_not_ready` on one whose first build has not produced an address yet.

**On the app's side, use `@usenaive-sdk/app`; never write sign-in yourself.** `enter(request, env)` serves `POST /api/enter`: it checks the ticket and answers with an `HttpOnly` session cookie and a redirect home. `viewer(request, env)` returns who is signed in (`sub`, `email`, `role`, `org`, `app`) or `null`; send a `null` visitor to `signInUrl(env)`. `naive(env)` is the API client for server code, using the key the owner approved for the app.

## What an app can do with Naive

`GET /v1/apps/{id}/permissions` — scope `agents:read`

```json theme={"system"}
{ "object": "app_permissions", "app_id": "app_…", "scopes": ["agents:read", "files:read"] }
```

The scopes of every live key linked to the app, in every mode, unioned: what `apps.request_access` was approved for, or what a template's apply minted. Empty when no linked key is live. Never a key id, prefix or value. A key set on the app by hand, or minted before keys named their app, is not linked and is not here: the Studio says so when the app holds a `NAIVE_API_KEY` this route does not account for. To take access away, revoke the key under API keys, or delete the app.

## Dashboard access

A hosted app that declares a generated secret named `DASHBOARD_PASSWORD` is password-protected by default: the platform generates a password-shaped value (`kq7m-x2rt-8bvn-pz4h`) once, alongside `DASHBOARD_TOKEN`, and keeps its own copy so it can be handed to a person.

`GET /v1/apps/{id}/access` — scope `agents:write`

```json theme={"system"}
{ "object": "app_access", "url": "https://your-app.example", "password": "kq7m-x2rt-8bvn-pz4h" }
```

`password` is `null` until one has been provisioned. This is the one route that returns a credential, deliberately and audited (`app.access_read`): the password exists to be typed into the dashboard's own sign-in form. Show it masked, copy it, never put it in a URL or a log line.

`POST /v1/apps/{id}/access` — scope `agents:write`, answers `201` with the same object after regenerating both `DASHBOARD_PASSWORD` and `DASHBOARD_TOKEN` through the app's secret path (host environment, secret record, republish). The dashboard's session cookie is the token, so every browser signed in to the dashboard is signed out; the Studio signs its own frames back in. Audited as `app.access_rotated`.

Both are refused with `feature_not_configured` on an app that has no hosting project.

## App MCP tools

A `fullstack` app can serve its own MCP endpoint and hand its tools to your agents with no per-agent wiring. Declare the path with `mcp` on create, or set it later:

```bash cURL theme={"system"}
curl -fsSL -X PATCH https://api.vetta.sh/v1/apps/app_01H... \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "mcp": "/mcp" }'
```

```typescript TypeScript theme={"system"}
await vetta.apps.update("app_01H...", { mcp: "/mcp" });
```

Setting `mcp` to a path — the first time, or to a different path — **mints a fresh opaque bearer token** and upserts it as the app secret `VETTA_MCP_TOKEN`. Your app reads it from its environment and requires `Authorization: Bearer <token>` on its MCP endpoint. The secret is listed by `GET /v1/apps/{id}/secrets` like any other and, like any other, its value is never returned by any route; the platform's copy is never on the wire either.

* Re-sending the current path is a no-op — the token is not rotated. To rotate, set `mcp` to `null` and then back to the path.
* Setting `mcp` to `null` deletes the `VETTA_MCP_TOKEN` secret and forgets the token.
* `mcp` on a `frontend_only` app is refused with `400 validation_failed` (param `mcp`), on create and on patch alike.

Once the app is `active`, every turn of every agent that can access it (the `apps` tool's `allowed_apps` — absent means every app in the organization) is offered the endpoint's tools as `<app-name>.<tool>`, e.g. `storefront.list_orders`, under the agent's normal `allow`/`ask`/`deny` policy. The platform calls `url + mcp` with the bearer injected server-side; the token never reaches the sandbox. An unreachable endpoint contributes no tools and never fails a turn. See [Tools from your apps](https://vetta.sh/docs/capabilities/tools.md#tools-from-your-apps).

## Custom domains

Connecting a domain points an existing org [domain](https://vetta.sh/docs/api/domains.md) at an app — it never registers one. The domain must already exist at `/v1/domains`; its `app_connect_status` track advances `pending → connected` as the hosting verifies it.

* `POST /v1/apps/{id}/domains` — scope `agents:write`. Body: `domain_id`, the id of an existing org domain.
* `GET /v1/apps/{id}/domains` — scope `agents:read`.
* `DELETE /v1/apps/{id}/domains/{domain}` — scope `agents:write`. By domain name.

## Store payments

A store app takes card payments on its owner's own Stripe account. Naive holds the only Stripe key. The app holds its own Naive key, which can open a checkout and read one, and never refund.

* `GET /v1/apps/{id}/payments` — the app's own key, or a signed-in owner or admin. The account's `stage` (`not_started`, `details_needed`, `in_review`, `ready`, `restricted`), `can_take_payments`, `can_pay_out`, `due_count`, `due_by` and `livemode`. 404 until the owner starts onboarding.
* `POST /v1/apps/{id}/payments/onboarding` — a signed-in owner or admin only; every API key is refused. Makes the account on first use and answers a single-use `url` to Stripe's onboarding. Body: `country` (two letters, default `US`).
* `POST /v1/apps/{id}/payments/checkouts` — the app's own key only. Opens a Stripe Checkout on the owner's account for lines the app priced: `order_ref`, `lines` (`name`, `unit_cents`, `quantity`, optional `image_url`), `countries`, `shipping_options`, `automatic_tax`, `success_url`, `cancel_url`, `expires_at`, and optional `email` (filled in on Stripe's page, and where Stripe sends the receipt). Stripe's page takes cards only. Answers `checkout_ref` and `url`. Refused until the account can take payments.
* `GET /v1/apps/{id}/payments/checkouts/{ref}` — the app's own key, or a signed-in owner or admin. Whether it is paid, the amounts in cents (`amount_cents`, `tax_cents`, `shipping_cents`, `refunded_cents`), the shopper's email, name and address, and `livemode`.
* `POST /v1/apps/{id}/payments/refunds` — a signed-in owner or admin only. Body: `checkout_ref`, `amount_cents`, and `seen_refunded_cents`, what the refund sheet showed as already refunded. Refused if that has changed or the amount is more than is left. Needs an `Idempotency-Key`.

### Money in without a store

An app with no server, like the `pay` template's page, gets paid through pay links and invoices on the owner's account. These routes take the app's template agent (through its `payments_request` tool, which asks the owner before every call) or a signed-in owner or admin. None of them refunds or pays out. `{id}` may be the app's id or its name.

* `GET /v1/apps/{id}/payments/links` — the app's pay links: `link_ref`, `url`, `active`, `name`, `unit_cents` (null when the payer chooses the amount).

* `POST /v1/apps/{id}/payments/links` — makes a pay link. Body: `name`, and `unit_cents` (50 or more), or none for a tip or deposit. Needs an `Idempotency-Key`.

* `GET /v1/apps/{id}/payments/invoices` — the newest invoices: `invoice_ref`, `number`, `status`, `email`, `amount_cents`, `paid_cents`, `url`, `due_at`.

* `POST /v1/apps/{id}/payments/invoices` — makes, finalizes and emails an invoice from Stripe. Body: `email`, optional `name`, `lines` (`description`, `cents`), `days_until_due` (1–90). Needs an `Idempotency-Key`.

* `GET /v1/apps/{id}/payments/received` — payments received in the last `days` (default 7, at most 90): `payment_ref`, `amount_cents`, `refunded_cents`, `email`, `name`, `created_at`.

* `POST /v1/webhooks/merchant_payments` — Stripe's signed callback for connected accounts. For an account event, Naive reads the account again. For a checkout, refund or dispute event (`checkout.session.*`, `charge.refunded`, `charge.dispute.created`), Naive posts `{ id, type, checkout_ref, order_ref, payment_ref }` to the app's `/api/orders/events` with the app's MCP bearer, and the app reads the checkout itself. Naive never acts on the body.

Store payments are off until the deployment sets `STORE_PAYMENTS_ENABLED=true`; until then every route answers 501.

## The app database

A `fullstack` app's managed database is its own primitive, documented on the [Database](https://vetta.sh/docs/api/database.md) page: `POST /v1/apps/{id}/db/query` runs SQL, `GET …/db/tables` lists the tables, `…/db/rest/{table}` is a REST layer over every table, and `…/db/migrations` is a migration ledger. `{id}` may be the word `default` to mean the organization's only `fullstack` app.

## App storage

A `fullstack` app also has an object store — buckets and objects, uploaded inline or through signed URLs, under `/v1/apps/{id}/storage/…`. See [Storage](https://vetta.sh/docs/api/storage.md).

## Installs

An **install** is one applied declaration — the whole project config, applied once, recorded once per `(organization, project)`. It exists because two writers can now apply the same declaration to one organization and neither can see the other: applying from a checkout and applying from the studio are the same act against the same rows, and without a shared record the second one silently reverts the first.

The install always **records** an apply. Whether it also **performs** one depends on where the bytes came from, and the request shape already says which: an apply that carries a `declaration` was reconciled by whoever sent it, and an apply that names a published `blueprint` and `template` without one is reconciled by the server, [below](#apply). Either way there is no second provisioning path — every resource is created and updated by its own idempotent route, keyed by its own name, and the hosted apply is a client of those same routes with your credential's scopes and your organization's limits.

**id** `string`

Unique id (e.g. `bpi_01H...`).

**object** `string`

Always `blueprint_install`.

**project** `string`

Your own project name — the same word an app's `project` stamps. Unique in the organization.

**blueprint** `string | null`

The blueprint the declaration resolved to, or `null` when it names none.

**template** `string | null`

Which of that blueprint's templates provides the crew.

**revision** `integer`

Counts **applies**, not versions of anything: `1` on the first, `+1` on each one after, whichever writer made it. It never rolls back.

**artifact\_version** `string | null`

The released version the declaration came from, or `null` when it was applied from a working tree. On an apply that names a published pair this is the version the server read — the newest publish of that pair — whatever the request asked for.

**answers** `object`

The setup answers this project was configured with, so a re-apply does not re-ask. Configuration only — a secret is a vault credential or an app secret, and neither is ever an answer. Editable after the apply with [`PATCH`](#edit-the-answers).

**selection** `object | null`

Which of the published crew this apply took: `{ agents: string[], apps: string[] }` of declared names, or `null` when the apply took everything. `null` on every row applied before selection existed.

**source** `string`

`studio` or `cli` — which writer applied this revision.

**report** `object | null`

What the apply did, or `null` when it only recorded — and `null` while it is still `pending`. One line per resource under `skills`, `identities`, `vaults`, `apps`, `agents`, `schedules` and `intake` — `{name, action, id?, url?, reason?}`, where `action` is `created`, `updated`, `unchanged`, `deleted` or `refused` — plus `skipped`, naming any live row the reconciler could not read and stepped over, and `deselected`, naming the declared agents and apps the `selection` left out. The two are distinct: `skipped` is something the reconciler could not do, `deselected` something it was told not to. A refusal is one line and never an escape: an app whose environment could not be resolved is `refused` beside a crew that was created.

**status** `string`

Whether **this revision's** reconcile has finished: `applied` (it has, or the apply only recorded), `pending` (it is running behind the response), `failed` (it threw), or `held` (the company is still being set up and has not been activated by a payment: the crew and its answers are recorded and nothing is built until it is, when the row moves to `pending`). `report` alone cannot say this — a `null` report is a record-only apply, a run that has not happened yet, and a run that failed, all three.

**failure** `string | null`

Why a `failed` reconcile threw, in one sentence. On an `applied` row, it names each `required` agent or app the apply refused, with the reason — the crew exists but cannot do the job it was installed for. `null` otherwise, and never a stack trace or a credential.

**applied\_at** `string`

When this revision was applied.

**context\_updated\_at** `string | null`

When the answers were last edited after the apply, or `null` when they never were.

**created\_at** `string`

When the project was first installed.

### Apply

`POST /v1/blueprints/installs` — scope `agents:write`

Returns `201` on the project's first apply and `200` on every one after — or `202`, when the apply is queued rather than run (below).

**Whether the apply provisions anything depends on where its bytes come from, and there is no flag for it.** Send a `declaration` and you have already reconciled it yourself — that is what `naive up` does — so this records the apply and `report` is `null`. Name a published `blueprint` and `template` and send none, and the server reads that declaration from the catalog and reconciles it for you, into your organization, as you: the resources it creates are exactly the ones you could have created by hand, with the same scopes, the same plan gate and the same limits. What it did comes back in `report`, and stays on the row.

**An apply made while the company is being set up is held.** Before a company is activated by a payment ([organizations](https://vetta.sh/docs/api/organizations.md)), an apply of a published pair answers **`202`** with `status: "held"`: the row, its answers and its names are recorded, and nothing is built. Only a published pair can be held, and it takes no `budgets` (set those once the company is activated); anything else is `402 subscription_required`. Once the company is activated, the payment itself starts every held row, and so does the next `GET /v1/blueprints/installs` — whichever comes first, each row once. It then runs exactly as a dashboard apply does, as the company's owner.

**An apply made from the dashboard runs after its response.** Provisioning a real crew was measured at 47–49 seconds, and nobody should be held on a request for it, so an apply made by a **signed-in person** against a published pair answers **`202`** immediately with `status: "pending"` and `report: null`, and reconciles behind that response. Poll `GET /v1/blueprints/installs?project=<project>`: the same row moves to `applied` with the `report` on it, or to `failed` with one sentence in `failure`. Nothing about *who* the apply runs as changes — the run is filed under the same person, with the role and scopes they hold **when it runs**, so a membership revoked in between refuses it.

**An apply made with an API key never does this.** A key carries no session claims to re-derive a credential from, so it reconciles inside its request and answers `201`/`200` with the finished `report`, exactly as it always has. So does a record-only apply, and so does every apply on a deploy that has no queue. If you are branching, branch on `status` rather than on the code.

**Only the owner adds a template to a company that runs one.** A signed-in admin or member who applies a new `project` in an organization that already has an install is refused `403 forbidden` (`only the company's owner can add a template`) before anything is written. They may still apply to a project the organization has, and anyone may make its first install. An API key is not held to this.

A published crew of more than 24 agents is refused `400 validation_failed` (param `template`) before anything is written, including the install row: a crew is a handful of people, and a release declaring otherwise is one to look at rather than to run.

**project** `string` *required*

Your own project name, up to 64 characters.

**declaration** `object`

The project config being applied, verbatim. Omit it only when `blueprint` and `template` name a [published artifact](#the-catalog) — the catalog holds that declaration and the apply reads it there. A working tree always has one.

The applied declaration is stored but never returned: it is what lets a `revision_conflict` name the paths two writers disagree at, and an apply that named a published pair was never given those bytes in the first place.

**blueprint** `string`

The blueprint this resolved to, or `null`.

**template** `string`

The template within it, or `null`.

**artifact\_version** `string`

The released version it came from. Omit when applying from a working tree — there is no version to name.

**answers** `object`

The setup answers, keyed by each question's `key`. Defaults to `{}`. Every key must be a question the published pair asks — an answer to a question nobody asked would be stored and never used, and is refused `400 validation_failed` (param `answers`) naming it. An answer to a declared question is always accepted: the crew reads it through its [project context](#project-context), whether or not an app's environment names it. Every question the pair asks must be answered, except one it marked `optional: true` — leave that key out to say nothing, and it is absent from the context rather than empty in it.

**selection** `object`

`{ agents: string[], apps: string[] }` — which of the published crew to provision, by declared name. **Omit it to take everything.** What it leaves out is not created, its timers are not armed and its first briefing is not sent, and the names come back under `report.deselected`. A name the pair does not declare, or a selection that leaves out an agent or app the template marks `required`, is refused `400 validation_failed` (param `selection`) naming every one of them, before anything is written. A resource an earlier apply created and a later apply deselects is **left running** — deselecting is not deleting.

**skill** `string | null`

The one skill this install is bound to — `slug` or `slug@N`, one your organization has (otherwise `400 validation_failed`, param `skill`, and nothing is written). Every apply of this install that names a published pair adds it to every agent's `skills`, and allows `read` on an agent that lists its tools, so the agent can open it; no other install gets it. **Omit it to keep the install's current skill** — a re-apply that does not mention it keeps it; send `null` to remove it (each agent goes back to exactly the skills the template declares). Used to run a [media skill](https://vetta.sh/docs/api/media-skills.md) as its own Short Form Social channel. The install's `skill` field says which skill it is bound to.

**source** `string`

`cli` (default) or `studio`.

**expected\_revision** `integer`

The revision you believe you are moving from. **Omitting it is a deliberate unconditional apply**, exactly as omitting `expected_version` is on `PATCH /v1/agents/{id}`: declarative CI should not have to read before every write. **`0` means "this project is new"**: the apply creates it, and if the project already exists it is refused `409 revision_conflict` and nothing is written — a retried or repeated create never sets up a second crew.

When a published template is set up as a **new** project and any of its agent, app or persona names is already used by another project in the organization, all of its names take the prefix `<project>-` (`history-producer`), and the install's `name_prefix` says so; the other project's resources are never touched. When every name is free, `name_prefix` is `""` and the names are the template's own. It is decided once, by the apply that creates the project. An install from before this rule has `name_prefix: null` and applies exactly as it did.

A mismatched `expected_revision` is refused `409 revision_conflict` (param `expected_revision`) and **nothing is written**. The message names the paths at which the stored declaration and yours disagree — `agents.2.system`, `apps.0.mcp` — not just the two revision numbers, so you can see whether the other writer touched anything you own. At most 20 paths are named, then a count of the rest.

`revision_conflict` is not `version_conflict`: the latter means one versioned object is stale and the fix is to re-read it, while this one means a whole shared declaration moved underneath you and the fix is a merge.

**Two applies of one project never RECONCILE at once**, whichever way they were made. Whatever performs the reconcile — this request, or the queued run behind a `202` — holds a lease on the project for as long as it takes, renewing it while it works, so the lease is released when the apply finishes rather than after a fixed time. That is what keeps reconciliation by name idempotent: without it two applies arriving together both create the same resources. Applies of different projects never wait for each other.

Where the wait lands differs by which apply it is:

* **An apply that reconciles inside its request** (an API key's, or any on a deploy without the queue) waits up to **60 seconds** for the one ahead of it and then applies normally, recording the next revision. One still running after that wait is refused with the retryable `409 job_not_ready` (param `project`) and nothing is recorded for it — retry it, and it will apply once the first has finished.
* **A queued apply is not held at all.** It records its revision and answers `202` at once; the *run* waits its turn behind the one ahead of it, and a run that never gets its turn ends as `failed` on the row with that reason in `failure` rather than as an error you receive. A second apply lodged while the first is still running supersedes it: the row is at the newer revision, only the newer run stamps a report, and the older one finds the row moved on and does nothing.

```bash cURL theme={"system"}
curl -fsSL https://api.vetta.sh/v1/blueprints/installs \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "project": "acme-agency", "blueprint": "agency", "template": "seo-geo",
        "expected_revision": 7, "answers": { "mailbox": "hello@acme.com" },
        "declaration": { "name": "acme-agency", "agents": [] } }'
```

```typescript TypeScript theme={"system"}
const install = await vetta.apps.installs.apply({
  project: "acme-agency",
  blueprint: "agency",
  template: "seo-geo",
  expected_revision: 7,
  answers: { mailbox: "hello@acme.com" },
  declaration,
});
console.log(install.revision); // 8
```

### List

`GET /v1/blueprints/installs` — scope `agents:read`. Every install in the organization, most recently applied first. Paginated with `limit` and `after`, and narrowed to one project with `project=<project>`. This is the read that watches a queued apply finish.

A row cannot sit `pending` forever waiting for a run that is never coming: past fifteen minutes — longer than any apply takes — this read stamps it `failed` with `the apply never finished and is no longer running; run it again`, so a client polling it is told to stop rather than told to keep waiting. Fifteen minutes is a plausible age, not a proof that nothing is running — a run mints its credential when it starts, so one delivered late still finishes normally — and a run that does finish always wins: the stamp is refused on any row that has already left `pending`, so a completed apply keeps its `applied` status and its report.

### Preview a new project

`GET /v1/blueprints/preview?blueprint=<blueprint>&template=<template>&project=<project>` — scope `agents:read`. Writes nothing.

What applying a published pair as a new `project` would call things in this organization, before you apply it: `{ object: "install_preview", project, blueprint, template, artifact_version, name_prefix, agents, apps, identities }`. When any name the pair declares is already taken here, every one of them starts with `<project>-` and `name_prefix` says so; otherwise it is `""` and the names are the declared ones. They are the names the apply then uses. A pair this deployment would refuse to apply is refused here the same way, and a pair nothing publishes is `404 not_found`.

```bash theme={"system"}
curl -fsSL "https://api.vetta.sh/v1/blueprints/preview?blueprint=media&template=faceless&project=faceless" \
  -H "authorization: Bearer sk_live_..."
```

### Edit the answers

`PATCH /v1/blueprints/installs/{id}` — scope `agents:write`. Returns the install.

The business changes its mind after the crew is provisioned — a new offer, a new audience — and re-running the apply to say so would re-provision a crew that is already standing. This rewrites the answers alone.

**answers** `object` *required*

The **whole** answer set, keyed by question `key`. It replaces what was stored; it does not merge, so a key you leave out is gone. Every key must be a question the install's published pair asks, refused `400 validation_failed` (param `answers`) otherwise, exactly as on the apply — and, exactly as on the apply, every question must be answered unless it is `optional: true`, which is left out rather than blanked.

The row's `revision` does not move — nothing was applied — and nothing is reconciled: the crew, its apps and its timers stand exactly as the last apply left them. `context_updated_at` is set, and the [project context](#project-context) reads the new answers at once.

An install applied from a working tree is validated against the questions of the declaration it applied. An install whose published release is no longer listed has no questions to validate against and is refused `400 validation_failed` (param `id`).

```bash theme={"system"}
curl -fsSL -X PATCH https://api.vetta.sh/v1/blueprints/installs/bpi_01H... \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "answers": { "offer": "Short explainers, for students", "tone": "Plain" } }'
```

### Remove a template

`DELETE /v1/blueprints/installs/{id}` — scope `agents:write`. Returns `{ id, object: "blueprint_install", deleted: true }`.

Takes one template out of the company. Only the company's **owner**, signed in as themselves, can do it. An API key, an admin and an agent's own token are refused `403 forbidden`.

* **What goes:** the install's agents — every agent whose `metadata.project` is the install's project. Each is fired as [`DELETE /v1/agents/{id}`](https://vetta.sh/docs/api/agents) fires one: its timers are deleted, its open cards stay on the board with no one on them, and it leaves every team that named it. The template's own board tasks that are not done go, unless a person commented on them, and so do its mini apps and its project context.
* **What stays:** its apps and their data, its personas and connected accounts, its vaults, its skills, files and chats, its finished tasks, and anything a person or another install made. Delete an app separately if you want it gone.
* **Money:** nothing is refunded or charged. The fired agents get no new tasks or timers. A chat that is already running finishes first, and is billed as usual.

It waits for an apply of the same project that is still running. An apply that lands while it runs is a `409 state_conflict`: the install stays, so remove it again. A second call, once the install is gone, is `404 not_found` and changes nothing.

```bash theme={"system"}
curl -fsSL -X DELETE https://api.vetta.sh/v1/blueprints/installs/bpi_01H... \
  -H "authorization: Bearer vt_..."
```

### Project context

`GET /v1/blueprints/installs/{id}/context` — scope `agents:read`.

What a template's crew knows about the business, as one object. It is what the crew's own read-only `project_context` tool returns to each agent, and the same read a dashboard makes — so what a person sees and what the agents are told never differ.

It is **derived**, not stored: from the install's answers, the questions it was applied with (the published pair's, or a working tree's own declaration), and the report of the latest apply (for a working tree, which records none, the project's live agents and apps). Only answer edits are ever written. It exists only for an install whose latest apply is `applied`; a `pending` or `failed` row answers `404 not_found`.

**object** `string`

Always `project_context`.

**project** `string`

The project name.

**blueprint** `string | null`

The blueprint, and

**template** `string | null`

the template that provided the crew.

**artifact\_version** `string | null`

The released version it was applied from.

**answers** `array`

`{ key, label, value }` — every answered question in the order the pair asks them, with the question's own wording as `label`. `value` is a string, or an array of strings for a multiple-choice question. A question left unanswered is not listed.

**apps** `array`

`{ name, url }` — the apps standing after the latest apply, with the address each was deployed at, or `null` when the apply did not record one.

**agents** `array`

`{ name, role }` — the crew standing after the latest apply, with the role the template gives each, or `null` when it names none.

**updated\_at** `string`

When the answers were last edited, or when the latest apply ran if they never were.

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/blueprints/installs/bpi_01H.../context \
  -H "authorization: Bearer sk_live_..."
```

## The catalog

A **blueprint artifact** is one published build of one blueprint × template × released version. It
is the same row for every organization — a hosted run does not fork a blueprint into a per-customer
copy, because there is nothing per-customer in the tree — so it carries no organization and nothing
you write ever reaches it.

Read it before you install: it is where the setup questions come from, and it is what lets a screen
show a person the crew, the apps and the daily cost of a company **before** anything is provisioned.

**id** `string`

`bpa_…`

**blueprint** `string`

The machine — its repository, screens and `/api/*`.

**template** `string`

Which of that blueprint's templates provides the crew.

**version** `string`

The blueprint repository's own released version.

**title** `string`

What a person calls the template ("Faceless channel"). Absent on a row published before it existed; show the `template` id then.

**description** `string`

One line on what the crew does. Absent when the template declares none.

**platforms** `array`

The publishing networks the template is made for, as platform ids (`youtube`, `tiktok`, …). For display only: the crew posts to the accounts connected to its persona, whatever this lists. Absent when the template declares none.

**trees** `array`

One built tree **per deployable app**, ordered as `apps` is. Empty for a data-only blueprint, which declares no app and ships only its crew. A blueprint ships as many apps as it declares — a marketing site and a private dashboard are two — so the digest is per app, not per release.

**trees\[]**

**app** `string`

The declared app name this tree builds, matching an entry in `apps`.

**content\_hash** `string`

Lowercase sha-256 of that app's built tree — the same digest a [deployment's `content_hash`](#deployments) carries, which is what lets an unchanged app skip its upload. Compare per app: a single digest over the whole release would move whenever any one app changed and re-upload the others for nothing.

Where the bytes live is not on the wire. A hosted apply fetches them itself; a client compares digests and uploads its own.

**questions** `array`

The setup questions — at most four — each a `{ key, label, type, help?, optional?, … }` field, the same shape a running agent asks with. Ask them before you apply, and send the answers on the apply. A field with `optional: true` may be left blank: omit its key from `answers`.

**schedules** `array`

What the crew does on a timer: `{ agent, cron, timezone, input, budget_micro_usd }`. `agent` is the declared **name**; no agent exists yet to have an id.

**agents** `array`

Who the crew is: `{ name, description, model, briefed, intake, intake_micro_usd, role, skills, tools, required }`. `briefed` is whether this agent is sent a first message the moment it is created; `intake` is that message, or `null`; `intake_micro_usd` is the integer micro-USD cap that first session spends under, present when `briefed` — sum it over the agents you select for the one-time cost of the apply. `role` is the job title the template gives the agent; `skills` the skill references it loads; `tools` the sorted names of the tools its toolset enables — built-in ones and `<app>.<tool>` ones alike; `required` whether an apply's `selection` may leave it out. `intake`, `role`, `skills`, `tools` and `required` are **absent** on a row published before they existed — the catalog is not rewritten by a release, so read them as optional.

**apps** `array`

What it deploys: `{ name, type, description, mcp, tools, required }`. `mcp` is the path the app serves MCP on, or `null`; `tools` is the tool names the crew's toolsets configure under that app; `required` whether a `selection` may leave it out. All three are **absent** on a row published before they existed, for the same reason `intake` is.

**crew\_per\_client** `array`

The agents the template creates **per client the operator signs**, in the same shape as `agents` — printed so a person can see the whole company before anything is provisioned, never created by an apply. Absent when the template declares none.

**identities** `array`

The personas the crew acts as: `{ name, description }`. Connect the accounts a crew posts from to its persona after the apply; the connected accounts are where it posts. Absent on a row published before it existed.

**published\_at** `string`

When this version was published.

**verified** `boolean`

"Naive verified": `true` on every Naive template, and on a template our reviewer verified. Today no company's template is verified.

### List

`GET /v1/blueprints/artifacts` — any key or session, including one not yet scoped to an
organization. Newest release first. Narrow it with
`blueprint` and `template`; paginate with `limit` and `after`.

```bash theme={"system"}
curl -fsSL "https://api.vetta.sh/v1/blueprints/artifacts?blueprint=media&template=faceless" \
  -H "authorization: Bearer sk_live_..."
```

Applying one of these takes no `declaration`: name its `blueprint` and `template` on the apply and
the newest published declaration for that pair is what gets recorded, along with that publish's
`version`. The declaration itself never comes back on the wire.

## Duplicate this company

Save your company's crew as a **private template** that only your organization can install, then set it up again as a new project.

What travels: each agent's name, role, instructions, model, budget, tool permissions, skills, schedules and team; the skills they use; and the setup questions, without your answers. Your own apps travel as short briefs — a name and what each does — that the new crew's CEO builds when you hand them over. What stays: keys and vaults, connected accounts, files, posts, memory, chats, domains, app data, setup answers, personas and board cards. The template sits on the template your company came from, or on `agency` · `blank`.

### Draft

`POST /v1/company_templates/draft` — scope `agents:write`. Exports the crew and rewrites company specifics out of every prompt: people's and clients' names, prices, places, links. Where the crew needs one, it adds a setup question instead. The rewrite is one model call, billed to your balance like a [proxy](https://vetta.sh/docs/api/proxy.md) call.

**project** `string`

Which project's crew. Defaults to the newest.

The answer is a `company_template_draft`: `blueprint` and `base_template` (the base), `project`, the `declaration` (`agents`, `questions`, `skills`, `briefs`), `rewrites` — each changed text as `{ label, before, after }` — and `skipped_skills`, the skills left out because they use your files.

### Save

`POST /v1/company_templates` — scope `agents:write`. Send the draft's `declaration` as you approved it, with a `name` (up to 80 characters) and the same `project`. It is checked like a creator's template — no links, no schedule more often than every 15 minutes — and answers `201` with the [catalog row](#the-catalog). Install it with `POST /v1/blueprints/installs`, naming its `blueprint` and `template`, a new `project` and `expected_revision: 0`. Any other organization gets `not_found`, and the public catalog never lists it.

### List

`GET /v1/company_templates` — scope `agents:read`. Your organization's private templates, newest first, as catalog rows. Narrow with `template`; paginate with `limit` and `after`.

### List the running company in the marketplace

Ask your CEO to list the company. Its `list_as_template` tool shows an owner or admin a card — what is listed, what the copy changes, what still blocks it and what you earn — and sends it for review once they approve. A crew company lists its crew; a channel company, one that runs a [media skill](https://vetta.sh/docs/api/media-skills.md) you built, lists that style with its samples. No private copy and no trial install first: the running company is what is listed.

`GET /v1/listings` — scope `agents:read`. Answers `listings`: your organization's own marketplace listings, newest first, each its newest submission as a `listing` plus `blueprint`, `template` and `taken_down`. `status` is `in_review`, `approved` (listed) or `changes_requested`, with the reviewer's `note`. `taken_down` is true once Naive suspended or unlisted it. `balance_micro_usd` is what your listings earned, net of refunds, that is not paid out yet, and `ready_micro_usd` the part past the hold that a payout would send now; both are `null` for a key without `billing:read`.

`GET /v1/listings/checks` — scope `agents:read`. Answers a `listing_checks`: `kind` (`crew` or `channel`), `project`, `changes` — what the listed copy tightens (tools that post, pay or build ask first, agents hand work to nobody, budgets come down to the marketplace ceilings), each as `{ path, message }` — `refusals`, what still blocks it and what to change, `skipped_skills`, the skills that stay behind because they use your files, and `listed`: every word that is listed (`path`, `label`, `text`), which the card shows in full, and `digest`, the SHA-256 of what is listed, which the card sends with its yes. Name another project with `?project=`; the newest is the default.

`POST /v1/listings` — scope `agents:write`, the `list_as_template` tool only, with the arguments an owner or admin approved on its card. Each approval lists once, and only while what is listed still has the `digest` the card showed; otherwise `state_conflict`, and the card is asked again. Send `name`, `tagline`, and optionally `project`, `hero_file_id` and `release_notes` (default `First release.`). Answers `201` with a `listing`: the `template_id` and its `template_submission` fields, `status: "in_review"`. We usually review within 5 business days. Once approved it is listed under your organization's name and earns it 15% of what each new company that starts with it pays its plan, for 12 months. A channel's listed style keeps its samples and the frames Naive generated, published when it is approved; the reels it was built from, their links and any still cut from them stay with you. A `hero_file_id` must be a video Naive generated in your company, or an edit of one. Listing a project again is the same template. Refused on any refusal the checks show (a link, a key, an email address, a phone number or an @handle anywhere in what is listed, the listing's own name, tagline and release notes included), while one is in review or listed, while 3 of your listings wait in review, and after a takedown.

### Pay out what it earned

Ask your CEO to pay out. Its `pay_out_earnings` tool shows an owner or admin a card, and once they approve it sends what your templates earned to your company's own Stripe account. Each share is held 30 days and refunds are taken off; a payout needs at least \$100 to have cleared. The first time, the company's owner sets up where the money goes: on the payout card, signed in, the owner presses **Set up with Stripe** and fills in Stripe's page. Stripe then pays it on to your bank.

`POST /v1/earnings/payouts` — scope `agents:write`, the `pay_out_earnings` tool only, with the arguments an owner or admin approved on its card. Each approval pays once. Send `country` (two letters, default `US`); the card hands it to the owner's set-up. Answers an `earnings_payout`: `status` is `paid` (with `payout_id`), `onboarding` (the owner has not finished setting up where it is paid) or `below_minimum`, and `amount_micro_usd` is what was paid, or what has cleared and is not yet paid. Asking again is safe: a payout that is set aside is paid once, under its own id. Answers `501` where payouts are not turned on.

`POST /v1/earnings/payouts/account` — scope `billing:write`, the company's owner signed in. Send `country` (two letters, default `US`). Makes the company's Stripe account the first time, with the owner's email, and answers an `earnings_payout_account`: `ready` when Stripe can pay it, otherwise `onboarding_url`, Stripe's page for the owner's details. An admin, a key or an agent is refused `403`. Answers `501` where payouts are not turned on.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.