PitchStation Apps Apps home Markdown for agents
PitchStation Apps · API reference

The Apps API

Every client — the web page, the AI skill, the MCP tools and the Apps client — is a caller of this API, so it is the complete surface. Machine-readable: /api/openapi-apps.json (OpenAPI 3; imports into a custom GPT as an Action).

AuthenticationConventionsOperationsEndpointsErrorsLimits

§01Base URL and authentication

Base URL https://pitchstation.ai; every path starts with /api/apps. Send a personal access token as Authorization: Bearer pst_…. The token's user must hold the app.deploy capability; without it every route answers 403 CAPABILITY_REQUIRED. A scoped token also needs the Apps scopes (apps:read, apps:deploy; the Apps preset at /tokens.html has them), or the route answers 403 SCOPE_REQUIRED. Accepting the publisher terms needs a signed-in browser session: a scoped token gets 403 SESSION_REQUIRED. Everything is owner-scoped: you see and change only your own apps and the targets you may use. Request and response bodies are JSON. The user manual explains each task step by step.

Clients take the origin from PITCHSTATION_URL and never hard-code it, so the same clients work against a self-hosted or separately hosted Apps service.

§02Conventions

ConventionWhat it means for you
Dry run by defaultDeploy, rollback, stop, PATCH env, resources, hardening, settings, domains, policy, POST backups, backup-config, restore, move and POST hosts preview unless the body says "dryRun": false. A dry run returns the redacted plan (never commands, env values or hashes) and changes nothing on a host; a deploy dry run that names an imageArchive uses an archive already uploaded. These act at once: POST /api/apps, DELETE /api/apps/{id}, PUT env, PUT targets/{id}/runtime, PUT and DELETE images, access links, operation retry, cancel and reconcile, move abort and remove-source, DELETE hosts/{hostId} and terms accept.
Plan, then applyDomains and policy changes are saved as a plan with an id. Apply by sending that planId. Plans expire after 15 minutes.
If-Match (ETag)Configuration changes carry the version you reviewed, e.g. "app-7-v2" from GET domain-config. A mismatch is CONFIG_VERSION_CONFLICT; a missing header is 428 PRECONDITION_REQUIRED.
Idempotency-KeyRequired to apply: 8–128 letters, digits, dots, underscores or hyphens. Repeating the identical request with the same key returns the original operation; the same key with a different request is IDEMPOTENCY_KEY_REUSED.
Prefer: respond-asyncDeploy and rollback return 202 at once with the operation, a Location header and statusUrl. Without it the server waits for the result for up to 15 minutes, but the production edge closes a silent request long before that (nginx after 300 seconds, the CDN in front of it possibly sooner), so send Prefer: respond-async and poll the operation.
Secrets stay inEnv values, basic-auth hashes, SSH commands and raw remote logs are never returned. Errors name the fields to fix, not their values.

§03Operations

An accepted deploy, rollback, domains, policy or env change becomes an operation with a UUID. Acceptance is not success.

StatusMeaning
queuedAccepted; waiting for the app's host lease.
runningSteps are executing on the host; phase and steps show where.
waiting_externalBlocked on something outside the platform, usually DNS. Fix it, then verify or retry.
cancel_requestedWill stop at the next safe checkpoint.
succeededDone; result carries the live URL and a readiness snapshot.
failed · cancelledStopped. error has a typed code. Retry re-runs only the failed safe steps.
A cleanup state of reconciliation_required means a remote outcome is uncertain (for example, a migration exited non-zero or the connection dropped mid-step). The platform will not guess: retry is refused until someone inspects the host and records what is actually there. The operation's reconcile.by says who: owner (the step touched only your containers — use the reconcile endpoint, the Apps page or the client) or operator (routing, certificates, a restore, or a quarantined host lease after a timeout or a dropped connection — an admin resolves it from the admin page, no restart). Database changes are never rolled back automatically.

§04Endpoints

Method and pathPurpose and notes
Targets
GET /api/apps/targetsHosts you may use: your own container hosts plus live shared hosts, each with shared, live, arch, region, free slots and committed versus capacity memory; and quota {poolApps, limit} for shared hosts; utilization is the host's last observed memory, disk and load (every 3 minutes). A shared host never shows its SSH address or other tenants' usage.
PUT /api/apps/targets/{id}/runtimeOperator: promote a registered host to {"runtime":"container","maxApps","cpuCores","memMb"}. One host can serve static websites and apps.
Apps and releases
GET /api/appsYour apps: status, current release, host port and the publishing summary (domains, access, route).
POST /api/appsCreate: {slug, name, targetId?, placement?, arch?, resources?, publication?, webService, containerPort, healthPath, migrateCmd?, migrateService?, hooks?, composeFile?, env?} → the app, its permanent hostPort and placement. Omit targetId to be placed (placement: {region?, arch?}; the response says why, and why each other host was rejected). publication: {access, password?, expiresAt?, applicationPolicy?} sets access from the first request — private with a password, or public with applicationPolicy: "none". Shared hosts count against your quota (QUOTA_EXCEEDED). A write, not a preview.
GET /api/apps/{id}App detail with its 20 most recent releases: the ids and timestamps you can roll back to, each release's status and health, and currentRelease. The client's releases --app ID prints them.
DELETE /api/apps/{id}Remove the registry record; it acts at once. On your own host it does not stop containers, and it keeps the app's hostnames reserved for good (even its slug cannot be created again), unless you add ?removeFromHost=1. On a shared host, or with that flag, the app must be stopped (APP_RUNNING); its containers, routing and budget slice are removed from the host, its hostnames and budget are released, and its data volumes are kept. A running move (MOVE_IN_PROGRESS) or a stopped copy an earlier move left on another host (HOST_STATE_CONFLICT) must be finished or removed first.
PUT /api/apps/{id}/envOlder env call: replaces the whole stored set, applied at the next deploy; it acts at once. On a managed app, an invalid key or value (not an identifier, PORT, HOST_PORT, COMPOSE_*, DOCKER_*, not a string, or multi-line) is refused as ENV_INVALID, without naming it. On a legacy app the set is stored unchecked, and the next deploy names any key it cannot write.
PATCH /api/apps/{id}/env{set, unset, scopes?, dryRun}. Preview by default; apply needs If-Match + Idempotency-Key and restarts the current release. Build-time keys (E11): scopes declares keys build or runtime (null returns a key to its default: build for NEXT_PUBLIC_*, VITE_*, REACT_APP_*, NUXT_PUBLIC_*, EXPO_PUBLIC_*, GATSBY_*, PUBLIC_*). A change to a build-time key only takes effect after a redeploy: the dry run lists buildScopedKeys with rebuildRequired, and the app carries buildStale until a deploy succeeds. Scopes sent alone are stored without a restart or an operation.
POST /api/apps/{id}/deploy{tarballBase64, dryRun, note?, releaseTs?, previewPassword?, expected?, imageArchive?}. Dry run by default. With Prefer: respond-async + key: 202 and an operation to poll. imageArchive (E1b): an uploaded archive id, or {appId, id} referencing an archive from another app you own. A live deploy retains its own archive copy before it queues, independently of source deletion; preview does not copy it. The host loads its images instead of building, and the dry run refuses a wrong architecture (IMAGE_ARCH_MISMATCH) or a built service without an archived image: (IMAGE_ARCHIVE_INCOMPLETE). A migration or hook naming a service the Compose file does not define is refused (DEPLOY_SETTINGS_INVALID).
PATCH /api/apps/{id}/tagsReplace owner labels with {tags:{customer:"acme"}} (or null to clear). At most 20 keys, 40 characters per key and 160 per value; no control characters or secrets. Requires apps:deploy. Tags appear in app list/detail and spend; GET /api/apps?customer=acme filters your own apps.
GET /api/apps/{id}/imagesThe app's image archives (E1b): id (the file's SHA-256), size, images with their tags and architecture; and the limits (maxMb, keep).
PUT /api/apps/{id}/imagesUpload docker save <image:tag>… | gzip as the raw body (Content-Type: application/octet-stream), in one request with Content-Length and X-Content-SHA256. At most 1,024 MB; the newest three per app are kept, plus the one the running release came from. 201 with the archive (200 if already uploaded). IMAGE_ARCHIVE_INVALID names the reason.
DELETE /api/apps/{id}/images/{archiveId}Remove an archive; refused (HOST_STATE_CONFLICT) for the one the running release was deployed from.
POST /api/apps/{id}/rollback{releaseId?, dryRun}. Preview by default; recreates a retained release with its Compose file and keeps the current domains, access, budget and last applied env. Without releaseId it goes to the newest healthy release other than currentRelease: normally the previous one, but after a rollback the newer release you rolled back from. The dry run names it in to (the release's release_ts). After a failed deploy a plain rollback skips the last good release, so name it. Images the host built come back only if each release tags them differently; images named by tag come back. Migrations are not undone.
POST /api/apps/{id}/previewReturns a one-time preview credential. It does not turn a gate on: use the policy operation.
POST /api/apps/{id}/stopDry run by default; {"dryRun":false} stops the containers and keeps volumes and releases.
Publishing configuration
GET /api/apps/publishing-capabilitiesWhat this server allows: configuration read, operations, whether live changes need the pilot, the adapter in use.
GET /api/apps/{id}/domain-configThe configuration and its ETag, saved plans, DNS records to add, recent operations and the last readiness.
PUT /api/apps/{id}/domain-configPlan {domains} (dry run default), or apply {"dryRun":false, planId}. If-Match always; Idempotency-Key to apply.
PATCH /api/apps/{id}/publication-policyPlan {policy:{access, password?, expiresAt?, eventAt?, applicationPolicy?, publicPaths?, cachePaths?, accounts?}} or apply a plan. Same version and key rules as domains. applicationPolicy declares the app's own restrictions (none for a public app with no login of its own). Private apps (milestone 4): publicPaths — up to five legacy path prefixes or exact {path:"/api/integrations/pitchstation/webhook", methods:["POST"]} entries served without the gate. Exact entries allow POST only (other methods return 405); expiry still applies and the app verifies signatures and replay protection; cachePaths — up to five prefixes whose responses may stay in the viewer's browser cache (a response the app marks immutable qualifies anywhere; everything else is no-store); accounts — {users:[username], teams:[id | handle | @owner/handle]} who may use Sign in with PitchStation on the login page.
POST /api/apps/{id}/domain-config/verify{planId?}: DNS and ownership check against authoritative and independent resolvers. No certificate, no host change.
Operations and evidence
GET /api/apps/{id}/operationsRecent operations for this app.
GET /api/apps/{id}/operations/{operationId}Status, phase, attempt, per-step results and the result (live URL, readiness).
POST /api/apps/{id}/operations/{operationId}/retryRetry the failed safe steps of the same operation. Refused when reconciliation is required.
POST /api/apps/{id}/operations/{operationId}/cancelRequest cancellation at the next safe checkpoint.
POST /api/apps/{id}/operations/{operationId}/reconcileResolve an uncertain operation yourself when its summary says reconcile.by: "owner" (the uncertain step touched only your containers). {"mode":"inspect"} pauses the app's queue of operations (the app keeps serving) and returns read-only host evidence and an inspectionId (15 minutes); {"mode":"confirm", inspectionId, attest:{migrationsReviewed:true, note}} records it and unblocks the app. The failed operation stays failed; deploy or roll back again to converge. Routing and host-lease cases are for an admin (RECONCILE_NOT_ALLOWED).
Status and logs (owner or admin)
GET /api/apps/{id}/statusThe app's containers: service, state, health, restarts, last exit code, out-of-memory kills, start time, image, and CPU/memory/process usage against the app's budget. Read-only (never waits for a deploy), cached 15 s; ?fresh=1 re-reads.
GET /api/apps/{id}/logs?service=&tail=&since=: recent container logs, interleaved by time. service must be one the status call reported; tail 1–1,000 (default 200); since like 10m or an ISO time; at most 256 KiB. Stored environment values and common token shapes are masked; each read is recorded. Ten host reads a minute per user and app (RATE_LIMITED).
PATCH /api/apps/{id}/resources{resources:{memMb, cpus, pids}, dryRun}: the app's budget (defaults 512 MB, 1 CPU, 256 processes). The dry run shows committed versus capacity. Where budgets are enforced (shared hosts), apply with an Idempotency-Key returns an operation that updates the app's slice live, without a restart; elsewhere it is stored and shown.
PATCH /api/apps/{id}/hardening{hardening:{readOnlyRootfs:["web"]}, dryRun}: run the listed services with a read-only root filesystem and a tmpfs at /tmp (write data to a named volume). Stored now, applied by the next deploy, rollback or env change. On a strict host (the host list says hardening: "strict") every service also runs with no-new-privileges, and the Compose file must pass the strict allowlist.
PATCH /api/apps/{id}/settings{settings:{migrateCmd?, migrateService?, hooks?, composeFile?}, dryRun} (E9, E8): the migration and the service it runs in; hooks.preDeploy / hooks.postDeploy, each up to five {service, cmd, timeoutSec} (pre-deploy hooks run after the images exist and before the migration and stop the deploy on failure; post-deploy hooks run after the health check and only warn); and the Compose file's path when it is not at the source root. null clears a setting. Stored now, used by the next deploy. Commands run inside the service's container as words (like sh splits them): shell operators outside single quotes are refused with DEPLOY_SETTINGS_INVALID; wrap one that needs a shell in sh -c '…'. Commands are never returned.
GET /api/apps/termsThe current publisher terms (version, text) and whether you accepted them. With APP_TERMS_REQUIRED=1, creating an app, a live deploy, and quoting or reserving an isolated host need them accepted (TERMS_REQUIRED); deploy dry runs do not.
POST /api/apps/terms/accept{version}: accept the current terms; recorded once per user and version. A signed-in browser session or a full-access token; a scoped token (the Apps preset included) gets 403 SESSION_REQUIRED, so people accept on the Apps page.
POST /api/app-abuse-reportsPublic, no account: {url, category, description, email?} with category one of malware, phishing, spam, illegal, copyright, harassment, other. Returns a reference; an admin reviews every report and can take the app down. Rate-limited per address (RATE_LIMITED). The form is at pitchstation.ai/abuse.html.
GET /api/apps/{id}/access-linksEach link also shows its use, opens: {count, firstOpenedAt, lastSeenAt}, which the host reports every five minutes (link ids and times only). Per-recipient access links of a private app, each with its state (active, expired, revoked) and, while active, its URL. A link opens the app without the password for one person, with a session of up to 12 hours each time it is opened; its sessions end when the link is revoked.
POST /api/apps/{id}/access-links{label, expiresAt?} (up to 50 active links per app, expiry within a year): returns the link and its URL https://<primary>/__ps/enter?t=… with 202 and an access-links operation that re-applies routing; the link works once it succeeds. Public apps have no links (POLICY_INVALID, field access).
DELETE /api/apps/{id}/access-links/{linkId}Revoke one link: 202 and an access-links operation; once it succeeds the link and every session it opened stop working.
GET /api/app-enter/{id}A browser navigation from a private app's login page, when the owner listed accounts: in the visitor's own PitchStation session (else to sign in and back), the owner, a listed person or a member of a listed team is sent to https://<primary>/__ps/enter with a five-minute hand-off token; anyone else sees a page saying they are not on the list. Membership is checked at every sign-in; turning accounts off ends every account session. Audited.
PATCH /api/apps/{id}/backup-config{backup, dryRun} with backup either {kind:"command", service, cmd, restoreCmd} (a command run inside one of your services whose output is the dump, and the one that reads it back from stdin) or {kind:"volumes", volumes:["data"]} (a gzipped tar of named volumes, restored with the app stopped), plus keep (1–10 on the host, default 5), schedule (none or daily) and beforeDeploy (default true). null removes it. The dry run shows the exact host command.
GET /api/apps/{id}/backupsThe configuration and the backups with their trigger (deploy, manual, scheduled), size, hash and where they are: on the host (the newest keep) and on the control plane (the newest 10, which also leave the server with the platform backups).
POST /api/apps/{id}/backups{dryRun}: take a backup now; apply returns 202 and a backup operation. Once backups are configured, a deploy of an app that already has a release also takes one first, unless beforeDeploy is false: before its first pre-deploy hook, else its migration, else its start.
GET /api/apps/{id}/backups/{backupId}/downloadThe control plane's copy of one backup, for you to keep.
POST /api/apps/{id}/restore{backupId, dryRun, confirm, review}: replace the app's data with a backup. The dry run returns plan.review and the original backup recipe targets; apply needs that review fingerprint, confirm set to the app slug and an Idempotency-Key (202, restore operation). A backup no longer on the host is copied back first. Changes to backup configuration cannot change an accepted restore. Historical backups without recipe metadata remain downloadable for manual recovery. A failed restore leaves the data state uncertain, and only an operator can reconcile it; code is never changed.
GET /api/apps/host-catalogIsolated hosts you can reserve: size classes S–XL (2–16 GB), arm64 or amd64, in us-west-2 (Oregon) or ap-southeast-1 (Singapore), each with its all-in monthly price (compute, disk, public IPv4). Both regions are priced from the us-west-2 (Oregon) table in this release, so a Singapore quote shows the Oregon price.
GET /api/apps/hostsYour reserved and running hosts (state, price, cost so far, address, apps) and your spend against your monthly cap.
POST /api/apps/hosts{name, sizeClass, arch, region, dryRun}: reserve an isolated host of your own. The dry run quotes the price and checks it against your monthly cap, which dev instances share (COST_CAP over it). Apply returns 201 reserved: an administrator launches it within a day. It runs strict Compose rules, user-namespace remapping and restricted egress, and is yours alone to place apps on once approved.
DELETE /api/apps/hosts/{hostId}Let a host go: a reservation is cancelled; a launched host with no apps is handed to the operator to terminate (HOST_STATE_CONFLICT while apps remain). It acts at once, with no dry run. hostId is the id GET /api/apps/hosts lists, not a target id.
GET /api/apps/spendYour monthly cap, what is committed (dev instances and app hosts), each host's cost so far, and each app's share of its host's price by memory budget, with who pays for it (you, or the platform on a shared host).
POST /api/apps/{id}/move{targetId | placement:{region, arch}, stateless, dryRun}: move the app to another host you may use. The dry run shows the destination and why, the hostname change (a platform hostname changes across base domains; custom domains keep their claim and need their DNS pointed at the new address as soon as the source stops, because the destination is routed only once every name resolves to it), how the data travels (a final backup, restored on the destination; stateless: true for apps without data) and the steps. Apply returns 202 and a move: pull the release, stop all source services, take the final volume backup with the source kept stopped, build on the destination (or load the release's image archive) and restore, switch, route there (DNS, certificates, readiness), keep the stopped source for 7 days. A failure before the switch starts the source again. Stateful moves require a volume backup covering all app data; command backups are refused with MOVE_UNSUPPORTED because no write-quiescence procedure is supported. After a restart, a switched move is finalized from a successful routing result or marked failed for destination recovery, retaining its stopped source without automatic removal.
GET /api/apps/{id}/movesThe app's moves with their state (accepted, running, switched, done, rolled_back, failed, interrupted) and every step.
GET /api/apps/{id}/moves/{moveId}One move.
POST /api/apps/{id}/moves/{moveId}/abortAbort an interrupted or queued move only while the app is still on its source (the source is started again). Already-switched moves must recover routing on the destination.
POST /api/apps/{id}/moves/{moveId}/remove-sourceAfter a finished move, remove the stopped source copy now instead of at the end of its retention: containers, routing and budget; data volumes are kept.
Readiness
GET /api/apps/{id}/readinessThe last readiness result (valid for 5 minutes).
POST /api/apps/{id}/readinessRun bounded public HTTPS checks now: DNS, TLS, access policy, identity marker and critical assets, per hostname.
GET /api/apps/{id}/publication-healthMonitor state and certificate status for each host.
GET /api/apps/{id}/publication-receiptA redacted receipt of what is published, safe to share (never passwords).

Reusable definitions: POST /api/apps accepts fromApp: ID for an app you own, with a new slug, target/placement and optional overrides. It copies web service, container port, health path, migration settings, Compose path, resources, architecture, hardening and backup configuration. Environments, credentials, gates, access links, domains, grants, hooks, releases and volume data are not copied. Choose a fresh publication policy when the template is private. Supply new environment values and optional tags.

Image evidence: publication receipts and completed deploy operations include imageDigest for the web service, imageDigests by service, and imageCapturedAt. These are actual container image IDs observed at deploy, frozen on the release. archiveDigest is the separate compressed archive checksum. imageEvidenceStatus is captured or unavailable; missing inspection evidence is null; rollback of mutable tags does not certify the original image is still running.

Operator assignment: an administrator may use PUT /api/admin/app-quotas/USERNAME with {maxPoolApps:25,tier:"operator",defaultResources:{memMb:512,cpus:1,pids:256}}. The ordinary quota of 3 and default 512 MB budget stay unchanged for other owners. Host slots and memory admission still apply. This is capacity policy, not a commercial price or a host purchase.

§05Errors

When the fix is a specific input, the error carries fields with the names (never values) to change — for example readiness returns ACCESS_POLICY_MISMATCH with fields: ["applicationPolicy"] for a public app whose owner has not declared none.

Errors are a fixed vocabulary: { code, status, retryable, actor, message, nextAction, fields?, appId?, operationId? }. actor says who must act: publisher (you) or operator (the people running the hosts).

CodeHTTPRetryActorNext action
DOMAIN_INVALID400nopublisherUse a hostname without a scheme, port, path, wildcard or IP address.
DOMAIN_ALREADY_BOUND409nopublisherThis hostname is reserved. Detach it from its current publication before reuse.
OWNERSHIP_UNVERIFIED409yespublisherAdd the displayed ownership record at your DNS provider, then verify again.
DNS_TARGET_MISMATCH409yespublisherReplace the conflicting DNS records with the displayed target records.
DNS_PROPAGATING409yespublisherDNS observations disagree or are unavailable. Wait for propagation, then verify again.
DNS_IPV6_CONFLICT409yespublisherRemove the unexpected AAAA record or configure an approved IPv6 target.
TLS_VALIDATION_FAILED502yesoperatorCheck the hostname, port 80 challenge path and certificate issuer diagnostics.
TLS_RATE_LIMITED429yesoperatorWait for the certificate issuer retry window before trying again.
ROUTE_APPLY_FAILED502yesoperatorRouting could not be applied. Check the recorded recovery outcome before retrying.
ACCESS_POLICY_MISMATCH409nopublisherObserved access differs from the requested policy. Check platform and application restrictions separately.
PUBLIC_CONTENT_MISMATCH409yespublisherVerify the expected application identity and critical assets; a successful HTTP status alone is insufficient.
PUBLICATION_EXPIRED409nopublisherRenew the platform expiry. Application-managed restrictions require an application change.
CONFIG_VERSION_CONFLICT409yespublisherReload the current configuration and make a new plan.
PRECONDITION_REQUIRED428nopublisherSend the current configuration ETag in If-Match.
IDEMPOTENCY_KEY_REUSED409nopublisherThis idempotency key was used for a different request. Use a new key for a new intent.
IDEMPOTENCY_KEY_REQUIRED400nopublisherSupply an Idempotency-Key of 8–128 printable letters, digits, dots, underscores or hyphens.
PLAN_EXPIRED409yespublisherThis plan has expired. Generate and review a new plan.
POLICY_INVALID400nopublisherCheck access, password, expiry and event date. Dates must include a timezone.
CONFIG_REVIEW_REQUIRED409nooperatorInventory the existing routes and restrictions before managing this legacy application.
ADAPTER_UNSUPPORTED409nooperatorThis target or application policy adapter is not supported for this operation.
LIVE_DISABLED409nooperatorLive publishing is switched off on this server. An administrator turns it on; dry-run planning is available.
OPERATION_BUSY409yespublisherAnother operation owns this application or target. Wait for it to settle.
OPERATION_NOT_RETRYABLE409nooperatorThis operation needs reconciliation, or has no safely retryable failed step.
OPERATION_CANCELLED409nopublisherThe operation was cancelled at a safe checkpoint. See effective state and cleanup status.
RECONCILIATION_REQUIRED409nooperatorA remote outcome is uncertain. Inspect the target before authorizing another mutation; database rollback is not automatic.
STEP_FAILED502yesoperatorThe deployment step failed. Inspect redacted step metadata and target diagnostics.
TARGET_UNREACHABLE502yesoperatorThe target could not be reached over SSH, so nothing was changed. Check the host, network and key, then retry.
TARGET_FENCED409yesoperatorA newer publisher owns this target, so this writer was stopped before changing anything. Retry to publish with a fresh lease.
SOURCE_INVALID400 · 413nopublisherThe source bundle is invalid, contains unsafe paths or secrets, or exceeds the upload limits. (413, with reason too-large and detail.limitBytes: the request body is over the server's upload limit.)
SOURCE_ROOT_REQUIRED400nooperatorServer-side sourceDir must be within an explicitly configured APP_SOURCE_ROOTS directory.
ENV_INVALID400nopublisherUse valid environment key names and string values; unset keys explicitly.
SECRET_UNAVAILABLE503nooperatorEncrypted publishing configuration is unavailable. Check DEPLOY_SECRET; no empty configuration was substituted.
PROBE_BLOCKED409nooperatorThe public probe destination is not an approved public address for this target.
NOT_FOUND404nopublisherNo such application, plan or operation.
INTAKE_FAILED500yesoperatorThe upload could not be processed because of an unexpected server error. Retry once; if it persists, report the correlationId.
QUERY_INVALID400nopublisherUse a service name reported by the status call, a tail of 1–1,000 lines and a since such as 10m, 2h or an ISO date.
RATE_LIMITED429yespublisherToo many requests of this kind: over 10 status or log reads a minute by one person for one app, or over 5 abuse reports in 10 minutes from one address (or 300 an hour in total). Wait for Retry-After where it is sent (status and logs), otherwise ten minutes, then try again.
HOST_NOT_APPROVED409nooperatorThis host is not approved for live publishing. An admin runs the host check and approves it; dry-run planning is available.
HOST_ONBOARDING_REFUSED409nooperatorThe host already serves sites this onboarding would affect. Review the listed files; tenant sites are never changed automatically.
COMPOSE_POLICY_VIOLATION400nopublisherThe Compose file uses a setting the platform does not allow on this host. fields names each one, e.g. services.web.privileged.
RESOURCES_INVALID400nopublisherUse memory of 64–16,384 MB, 0.1–16 CPUs and 32–4,096 processes.
RESOURCE_ADMISSION409nopublisherThis budget would exceed the host capacity after its reserve. Lower the budget or choose another host; detail shows the arithmetic.
RESOURCES_UNSUPPORTED409nooperatorThis host cannot enforce per-app budgets (it needs the systemd cgroup driver on cgroup v2). Run the host check.
HOST_FULL409nopublisherThat host has no free application slot. Choose another host or remove an app (formerly TARGET_FULL).
PLACEMENT_UNAVAILABLE409yespublisherNo shared host has a free slot and budget headroom for this request; detail.rejected says why for each. Try another region or architecture.
QUOTA_EXCEEDED409nopublisherYou have reached your quota of apps on shared hosts. Delete one or ask an admin to raise it.
APP_RUNNING409nopublisherStop the app before deleting it: an app on a shared host must not leave containers running without an owner.
RECONCILE_NOT_ALLOWED409nooperatorThis uncertainty involves the host itself (routing, nginx or the host lease), so an operator must reconcile it.
INSPECTION_EXPIRED409yespublisherThe inspection is missing, older than 15 minutes, or the operation changed since. Inspect again.
ATTESTATION_REQUIRED400nopublisherConfirm you reviewed the migration state (attest.migrationsReviewed: true) and describe what you found.
APP_TAKEN_DOWN409nooperatorAn administrator has taken this app down (abuse or terms). It cannot be deployed or re-enabled until the takedown is lifted.
TERMS_REQUIRED403nopublisherAccept the current PitchStation Apps terms on the Apps page (/apps.html) while signed in; a scoped API token cannot accept them. They are required before creating an app, a live deploy, and quoting or reserving an isolated host.
REPORT_INVALID400nopublisherGive the app address (https://…), one of the listed categories and a description of 10–4,000 characters.
BACKUP_NOT_CONFIGURED409nopublisherSay how this app's data is backed up first (PATCH /api/apps/{id}/backup-config); a restore of a command backup also needs its restoreCmd.
BACKUP_FAILED502yespublisherThe backup produced nothing usable. Read the step output, fix the command, service or volume names, and try again; the running app was not changed.
RESTORE_REVIEW_REQUIRED409nopublisherReview a fresh restore dry run and send its review fingerprint. Backups without recorded recipes require manual recovery.
RESTORE_CONFIRMATION_REQUIRED400nopublisherA restore replaces the app's data. Review the dry run, then send confirm with the app's slug.
COST_CAP402nopublisherThis host would take your committed monthly spend (dev instances and app hosts) over your cap. Choose a smaller size or ask an admin to raise the cap.
HOST_STATE_CONFLICT409nopublisherThe host is not in a state that allows this (for example, it still runs apps, or it was already launched or terminated).
DEPLOY_SETTINGS_INVALID400nopublisherCheck the deploy setting named in fields: a Compose service name, a command without shell operators (wrap one that needs a shell in sh -c '…'), at most five hooks per stage of 1–1,800 seconds, and a Compose file path inside the source.
IMAGE_ARCHIVE_INVALID400nopublisherThe image archive was refused (detail.reason): upload the output of docker save <image:tag>… | gzip in one request, with its SHA-256 in X-Content-SHA256, within the size limit.
IMAGE_ARCHIVE_UNAVAILABLE409nopublisherThat image archive is not on the server (never uploaded, or removed after newer uploads). Upload it again (PUT /api/apps/{id}/images) and deploy with its id.
IMAGE_ARCH_MISMATCH409nopublisherAn image in the archive is built for a different CPU architecture than the host (detail). Build for the host (docker build --platform linux/arm64 or linux/amd64) or choose a host that matches.
IMAGE_ARCHIVE_INCOMPLETE409nopublisherWith an image archive nothing is built on the host, so every service with build: needs an image: tag the archive holds (fields and detail name them).
BUILD_LIMIT_EXCEEDED409nopublisherThe image build ran out of the host's build memory (detail.limitMb) and was stopped; nothing else changed. Make the build lighter, or build elsewhere and deploy an image archive (PUT /api/apps/{id}/images).
MOVE_UNSUPPORTED409nopublisherThis move cannot be made as asked (see detail.reason): for example, the app has no release, its data is too large to carry, or both hosts share one wildcard without per-app DNS.
MOVE_IN_PROGRESS409yespublisherThe app is being moved to another host. Wait for the move to finish (GET its moves) before changing it.

These seven codes come from outside the typed catalogue: their responses carry code and error, without retryable or actor.

CodeHTTPWhenNext action
CAPABILITY_REQUIRED403The token's user lacks the app.deploy capability (capability names it).Ask an administrator.
SESSION_REQUIRED403A scoped token called a route that needs a web session: accepting the publisher terms.Accept the terms on the Apps page while signed in.
SCOPE_REQUIRED403A scoped token lacks the scope the route needs (required names it).Make a token with the Apps preset at /tokens.html.
NOT_CONTAINER_TARGET400 · 409The host is registered for static websites, not containers.Use a container host from GET /api/apps/targets, or ask an operator to register the host with runtime: container.
NO_SECRET503DEPLOY_SECRET is not configured on the server, so env values cannot be stored encrypted.Tell the operator.
BAD_SLUG400The slug is not 1–40 lowercase letters, digits and hyphens, starting and ending with a letter or digit. Capitals are lowercased for you.Fix the slug.
NO_PORT400The host has no free port left in its app range (41000–41999).Choose another host.

§06Limits

PitchStation Apps documentation · updated 2 October 2026 · Markdown version · OpenAPI · Overview · User manual · Deploy guide · Architecture · Apps home: pitchstation.ai/apps

Host admission counts current apps, queued move destinations and pending resource increases together. A resource decrease frees capacity only after the host confirms success. Failed or interrupted destination cleanup retains its port and capacity reservation until recovery succeeds. Forgetting a deployed app retains its allocation because its containers may remain on the host. Stop and takedown verify that no project container remains running; takedown results separately report owner blocking, routing containment and process termination.