> ## 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](/docs/api/domains): 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

<ResponseField name="id" type="string">Unique id (e.g. `app_01H...`).</ResponseField>
<ResponseField name="object" type="string">Always `app`.</ResponseField>
<ResponseField name="name" type="string">A lowercase DNS label, unique in the organization.</ResponseField>
<ResponseField name="description" type="string | null">Free text.</ResponseField>
<ResponseField name="type" type="string">`frontend_only` or `fullstack`.</ResponseField>
<ResponseField name="status" type="string">`provisioning`, `active`, or `error`. A `frontend_only` app is `active` at birth; a `fullstack` app is `provisioning` until its database is ready.</ResponseField>
<ResponseField name="url" type="string | null">The app's live URL.</ResponseField>
<ResponseField name="mcp" type="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).</ResponseField>
<ResponseField name="error" type="string | null">Set when `status` is `error`.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>

## Create an app

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

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

<ParamField body="name" type="string" required>A lowercase DNS label, 63 characters or fewer.</ParamField>
<ParamField body="description" type="string">Optional, up to 500 characters.</ParamField>
<ParamField body="type" 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.</ParamField>
<ParamField body="mcp" type="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).</ParamField>

<CodeGroup>
  ```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"
  ```
</CodeGroup>

## 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`

<ParamField body="files" type="object" required>Path → base64 contents, e.g. `{ "index.html": "PGgxPuKAplPC9oMT4=" }`. At least one file.</ParamField>
<ParamField body="content_hash" type="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.</ParamField>

<ResponseField name="id" type="string">Deployment id (e.g. `apd_01H...`).</ResponseField>
<ResponseField name="object" type="string">Always `app_deployment`.</ResponseField>
<ResponseField name="status" type="string">`queued`, `building`, `ready`, or `error`.</ResponseField>
<ResponseField name="url" type="string | null">The deployment's URL once ready.</ResponseField>
<ResponseField name="error" type="string | null">Set when the build failed.</ResponseField>
<ResponseField name="content_hash" type="string | null">The `content_hash` the deploy was created with; `null` when none was sent.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>

## 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": "1757160000000.f3Xq…" }
```

`ticket` is an HMAC keyed by the app's token over its own expiry, good for two minutes. It is **not** the token, and the token is still returned by no route.

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

## 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:

<CodeGroup>
  ```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" });
  ```
</CodeGroup>

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](/docs/capabilities/tools#tools-from-your-apps).

## Custom domains

Connecting a domain points an existing org [domain](/docs/api/domains) 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.

## The app database

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

Runs SQL against the app's own managed database. `fullstack` apps only, and only once the app is `active`: a `frontend_only` app answers `501 feature_not_configured`, and a still-provisioning backend answers a retryable `job_not_ready`.

<ParamField body="query" type="string" required>The SQL to run.</ParamField>

<ResponseExample>
  ```json Response theme={"system"}
  {
    "object": "app_db_result",
    "rows": [{ "count": 42 }],
    "row_count": 1
  }
  ```
</ResponseExample>

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

<ResponseField name="id" type="string">Unique id (e.g. `bpi_01H...`).</ResponseField>
<ResponseField name="object" type="string">Always `blueprint_install`.</ResponseField>
<ResponseField name="project" type="string">Your own project name — the same word an app's `project` stamps. Unique in the organization.</ResponseField>
<ResponseField name="blueprint" type="string | null">The blueprint the declaration resolved to, or `null` when it names none.</ResponseField>
<ResponseField name="template" type="string | null">Which of that blueprint's templates provides the crew.</ResponseField>
<ResponseField name="revision" type="integer">Counts **applies**, not versions of anything: `1` on the first, `+1` on each one after, whichever writer made it. It never rolls back.</ResponseField>
<ResponseField name="artifact_version" type="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.</ResponseField>
<ResponseField name="answers" type="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.</ResponseField>
<ResponseField name="source" type="string">`studio` or `cli` — which writer applied this revision.</ResponseField>
<ResponseField name="report" type="object | null">What the apply did, or `null` when it only recorded. 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. 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.</ResponseField>
<ResponseField name="applied_at" type="string">When this revision was applied.</ResponseField>
<ResponseField name="created_at" type="string">When the project was first installed.</ResponseField>

### Apply

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

Returns `201` on the project's first apply and `200` on every one after.

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

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.

<ParamField body="project" type="string" required>Your own project name, up to 64 characters.</ParamField>

<ParamField body="declaration" type="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.
</ParamField>

<ParamField body="blueprint" type="string">The blueprint this resolved to, or `null`.</ParamField>
<ParamField body="template" type="string">The template within it, or `null`.</ParamField>
<ParamField body="artifact_version" type="string">The released version it came from. Omit when applying from a working tree — there is no version to name.</ParamField>
<ParamField body="answers" type="object">The setup answers, keyed by each question's `key`. Defaults to `{}`.</ParamField>
<ParamField body="source" type="string">`cli` (default) or `studio`.</ParamField>
<ParamField body="expected_revision" type="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.</ParamField>

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 run at once.** An apply holds a lease on its project for as long as it takes — it renews that lease while it works, so the lease is released when the apply finishes rather than after a fixed time. A second apply of the same project **waits up to 60 seconds** for the first and then applies normally, recording the next revision; that is what keeps reconciliation by name idempotent, because without it two applies that arrive together both create the same resources. An apply 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. Applies of different projects never wait for each other.

<CodeGroup>
  ```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
  ```
</CodeGroup>

### List

`GET /v1/blueprints/installs` — scope `agents:read`. Every install in the organization, most recently applied first. Paginated with `limit` and `after`.

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

<ResponseField name="id" type="string">`bpa_…`</ResponseField>
<ResponseField name="blueprint" type="string">The machine — its repository, screens and `/api/*`.</ResponseField>
<ResponseField name="template" type="string">Which of that blueprint's templates provides the crew.</ResponseField>
<ResponseField name="version" type="string">The blueprint repository's own released version.</ResponseField>

<ResponseField name="trees" type="array">
  One built tree **per deployable app**, ordered as `apps` is, and never empty. 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.

  <Expandable title="trees[]">
    <ResponseField name="app" type="string">The declared app name this tree builds, matching an entry in `apps`.</ResponseField>
    <ResponseField name="content_hash" type="string">Lowercase sha-256 of that app's built tree — the same digest a [deployment's `content_hash`](#deploy) 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.</ResponseField>
  </Expandable>

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

<ResponseField name="questions" type="array">The setup questions, each a `{ key, label, type, … }` field — the same shape a running agent asks with. Ask them before you apply, and send the answers on the apply.</ResponseField>
<ResponseField name="schedules" type="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.</ResponseField>
<ResponseField name="agents" type="array">Who the crew is: `{ name, description, model, briefed }`. `briefed` is whether this agent is sent a first message the moment it is created.</ResponseField>
<ResponseField name="apps" type="array">What it deploys: `{ name, type, description }`.</ResponseField>
<ResponseField name="published_at" type="string">When this version was published.</ResponseField>

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