Skip to main content
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: 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

string
Unique id (e.g. app_01H...).
string
Always app.
string
A lowercase DNS label, unique in the organization.
string | null
Free text.
string
frontend_only or fullstack.
string
provisioning, active, or error. A frontend_only app is active at birth; a fullstack app is provisioning until its database is ready.
string | null
The app’s live URL.
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.
string | null
Set when status is error.
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.
string
required
A lowercase DNS label, 63 characters or fewer.
string
Optional, up to 500 characters.
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.
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.

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); 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
object
required
Path → base64 contents, e.g. { "index.html": "PGgxPuKAplPC9oMT4=" }. At least one file.
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.
string
Deployment id (e.g. apd_01H...).
string
Always app_deployment.
string
queued, building, ready, or error.
string | null
The deployment’s URL once ready.
string | null
Set when the build failed.
string | null
The content_hash the deploy was created with; null when none was sent.
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.
  • GET /v1/apps/{id}/deployments/{apd}/files — scope agents:read
string
Always app_deployment_files.
string
The deployment the files belong to.
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
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.

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

Custom domains

Connecting a domain points an existing org domain 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

A fullstack app’s managed database is its own primitive, documented on the Database 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.

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. 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.
string
Unique id (e.g. bpi_01H...).
string
Always blueprint_install.
string
Your own project name — the same word an app’s project stamps. Unique in the organization.
string | null
The blueprint the declaration resolved to, or null when it names none.
string | null
Which of that blueprint’s templates provides the crew.
integer
Counts applies, not versions of anything: 1 on the first, +1 on each one after, whichever writer made it. It never rolls back.
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.
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.
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.
string
studio or cli — which writer applied this revision.
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.
string
Whether this revision’s reconcile has finished: applied (it has, or the apply only recorded), pending (it is running behind the response), or failed (it threw). 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.
string | null
Why a failed reconcile threw, in one sentence. null on every other row, and never a stack trace or a credential.
string
When this revision was applied.
string | null
When the answers were last edited after the apply, or null when they never were.
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 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. 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.
string
required
Your own project name, up to 64 characters.
object
The project config being applied, verbatim. Omit it only when blueprint and template name a published artifact — 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.
string
The blueprint this resolved to, or null.
string
The template within it, or null.
string
The released version it came from. Omit when applying from a working tree — there is no version to name.
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, 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.
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.
string
cli (default) or studio.
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.
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.

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.

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.
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 reads the new answers at once. An install applied from a working tree has no published questions to validate against and is refused 400 validation_failed (param id): apply the tree again with the new answers.

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 published pair’s questions, and the report of the latest apply. Only answer edits are ever written. It exists only for an install whose latest apply is applied; a pending or failed row, or one applied from a working tree, answers 404 not_found.
string
Always project_context.
string
The project name.
string | null
The blueprint, and
string | null
the template that provided the crew.
string | null
The released version it was applied from.
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.
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.
array
{ name, role } — the crew standing after the latest apply, with the role the template gives each, or null when it names none.
string
When the answers were last edited, or when the latest apply ran if they never were.

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.
string
bpa_…
string
The machine — its repository, screens and /api/*.
string
Which of that blueprint’s templates provides the crew.
string
The blueprint repository’s own released version.
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.Where the bytes live is not on the wire. A hosted apply fetches them itself; a client compares digests and uploads its own.
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.
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.
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.
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.
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.
array
The personas the crew acts as: { name, description, connections }, where connections.social — present only when the template declares it — names the choice question (from) whose picked options map (map) to the publishing networks to connect to that persona before the crew is provisioned. Absent on a row published before it existed.
string
When this version was published.

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