{"tags":[{"name":"Health","description":"Health check endpoints"},{"name":"Me","description":"Caller identity endpoints"},{"name":"Orgs","description":"Organization endpoints"},{"name":"Members","description":"Org membership — list, invite, change roles, remove"},{"name":"Agents","description":"Agent + integration members — create, list, revoke; mint org-scoped tokens"},{"name":"Secrets","description":"Org secret store — encrypted credentials, write-only values"},{"name":"Billing","description":"Org billing reads — wallet, usage, ledger"},{"name":"Apps","description":"App endpoints — projects within an org"},{"name":"Artifacts","description":"Uploaded files — a design's own assets (logo, hero photo) and files an agent reads"},{"name":"Preview","description":"Render an app's live design-time state to a PNG screenshot"},{"name":"Deployments","description":"App deployment status — list and inspect deployments per app"},{"name":"Executions","description":"Workflow runs — trigger pre-deploy executions and observe them as async jobs"},{"name":"Translations","description":"An app's languages — read what is translated and run the translator"},{"name":"Blocks","description":"Block vocabulary — composable UI + workflow units to discover and configure"},{"name":"Recipes","description":"Recipe compositions — reusable sections and pages built from blocks"},{"name":"Images","description":"Stock-photo catalog — hotlinkable, attributed photography for authored apps"},{"name":"Scratchpad","description":"The board an app is thought out on — cards, links, and the files a person brought"},{"name":"Workspaces","description":"Mockup / workflow / entity workspaces — list per app, fetch the full node tree by id"},{"name":"Tokens","description":"API token CRUD"},{"name":"Device","description":"RFC 8628 device-authorization flow — how CLIs mint their first token"}],"security":[{"bearerAuth":[]}],"externalDocs":{"description":"Full documentation","url":"https://docs.orbitapps.io"},"paths":{"/api/v1/deployments/{id}/domains/{hostname}/verify":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."},{"in":"path","name":"hostname","required":true,"schema":{"type":"string"},"description":"The attached hostname"}],"responses":{"200":{"description":"The refreshed domain entry.","content":{"application/json":{"schema":{"title":"CustomDomain","description":"One custom domain attached to the deployment, with the DNS record the owner must create.","type":"object","properties":{"hostname":{"description":"The customer-owned hostname (e.g. shop.acme.com).","oneOf":[{"type":"string"},{"type":"null"}]},"kind":{"description":"cname (subdomain) | apex (root domain, A record).","oneOf":[{"type":"string"},{"type":"null"}]},"dns_status":{"description":"pending | verified | failed.","oneOf":[{"type":"string"},{"type":"null"}]},"cert_status":{"description":"none | issuing | ready | failed.","oneOf":[{"type":"string"},{"type":"null"}]},"cert_reason":{"description":"Why the cert failed (e.g. secret-quota-exhausted, issuing-timeout, apply-rejected).","oneOf":[{"type":"string"},{"type":"null"}]},"dns_type":{"description":"The record type to create: CNAME or A.","oneOf":[{"type":"string"},{"type":"null"}]},"dns_target":{"description":"What the record must point at — the app host (CNAME) or the load balancer IP (A).","oneOf":[{"type":"string"},{"type":"null"}]},"checked_at":{"description":"Last DNS check.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["hostname","kind","dns_status","cert_status","dns_type","dns_target"],"additionalProperties":false}}}},"403":{"description":"Caller lacks the role to manage domains.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Deployment or hostname not found / not visible.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Verify a custom domain's DNS","description":"Re-checks that the hostname resolves to our load balancer. On a match: `dns_status` flips to `verified`, the Ingress is applied, and certificate issuance starts (`cert_status: issuing`, then `ready` — a background reconciler also re-checks pending domains every ~60s). On a mismatch: `failed`; fix the DNS record and verify again — the verb is idempotent.\n\nAuthorization: `[:manage :resource]` (owner/admin)."}},"/api/v1/apps":{"get":{"parameters":[{"in":"query","name":"org_id","required":false,"schema":{"type":"string","format":"uuid"},"description":"Restrict the result to apps in this org. Profile-scoped tokens may omit it to list across every org they're an active member of; passing an org_id the caller can't see yields an empty data array, same opacity as /orgs."}],"responses":{"200":{"description":"Visible apps.","content":{"application/json":{"schema":{"title":"AppList","description":"Paginated-ready envelope. v1 returns the entire visible set in `data`; future revisions may add pagination cursors alongside.","type":"object","properties":{"data":{"type":"array","items":{"title":"App","description":"Public projection of an app the caller can see. Mutable fields are exposed via PATCH /api/v1/apps/{id}.","type":"object","properties":{"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"idea_id":{"description":"Linked idea id, when the app was scaffolded from an idea. May be null.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"archived":{"description":"True once the app has been archived. Archive/delete verbs are not yet exposed in v1.","type":"boolean"},"slug":{"description":"URL-safe identifier, unique within the org. Lowercase letters, digits, and hyphens; 1-60 chars.","type":"string"},"org_id":{"description":"Owning org id. Immutable: apps cannot be transferred between orgs via this api.","type":"string","format":"uuid"},"settings":{"description":"Free-form app-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"icon":{"description":"Single-glyph emoji shown alongside the title. Defaults to ✨.","oneOf":[{"type":"string"},{"type":"null"}]},"themes":{"description":"Theme catalog: {active, definitions}. Drives end-user theming in the canvas.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"title":{"description":"Human-readable name shown in UI. 1-200 chars.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"},"preview":{"description":"Optional preview image URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"locales":{"description":"The languages this app serves: `active` (the one it is authored in), `available` (the others it offers) and `glossary` (names a translator must not translate). Read what is translated, and run the translator, at GET /api/v1/apps/{app_id}/translations.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"}},"required":["id","org_id","slug","title","icon","archived","settings","themes","locales","created_at","updated_at"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}}},"tags":["Apps"],"summary":"List the caller's apps","description":"Returns every app the calling token can read.\n\nScope semantics:\n  • profile-scoped tokens → all apps in every org where the caller has an active member row.\n  • org-scoped tokens     → only apps in the token's bound org.\n\nUse `?org_id=<uuid>` to filter to a single org. Passing an org_id the caller can't see returns an empty `data` array rather than 403 — same Stripe-style opacity as /orgs.\n\nChain this after GET /api/v1/orgs to enumerate the apps addressable for subsequent /api/v1/apps/{id} requests."},"post":{"requestBody":{"content":{"application/json":{"schema":{"title":"AppCreate","description":"Request body for POST /api/v1/apps. `org_id` is required; the calling token must have an active member row in that org and a role granting `[:write :resource]` (owner, admin, or member).","type":"object","properties":{"description":{"description":"Optional short blurb.","oneOf":[{"type":"string"},{"type":"null"}]},"slug":{"description":"URL-safe identifier; unique within the org.","allOf":[{"type":"string"},{}]},"org_id":{"description":"Owning org. The token must be authorized to write here.","type":"string","format":"uuid"},"settings":{"description":"Free-form app-level settings map. Merged over the server's defaults (which author `runtime`), so omitting it is the normal case and any key you send wins.","type":"object","additionalProperties":{}},"icon":{"description":"Single-glyph emoji shown alongside the title. Defaults to ✨ when omitted.","oneOf":[{"type":"string"},{"type":"null"}]},"themes":{"description":"Theme catalog, which also seeds the theme a reset returns to. Defaults to {}.","type":"object","additionalProperties":{}},"title":{"description":"Human-readable name shown in UI. 1-200 chars.","allOf":[{"type":"string"},{}]},"preview":{"description":"Optional preview image URL.","oneOf":[{"type":"string"},{"type":"null"}]},"locales":{"description":"Locale catalog. Defaults to {}.","type":"object","additionalProperties":{}}},"required":["org_id","slug","title"],"additionalProperties":false}}}},"responses":{"201":{"description":"The created app row, with atlas-stamped id + timestamps.","content":{"application/json":{"schema":{"title":"App","description":"Public projection of an app the caller can see. Mutable fields are exposed via PATCH /api/v1/apps/{id}.","type":"object","properties":{"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"idea_id":{"description":"Linked idea id, when the app was scaffolded from an idea. May be null.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"archived":{"description":"True once the app has been archived. Archive/delete verbs are not yet exposed in v1.","type":"boolean"},"slug":{"description":"URL-safe identifier, unique within the org. Lowercase letters, digits, and hyphens; 1-60 chars.","type":"string"},"org_id":{"description":"Owning org id. Immutable: apps cannot be transferred between orgs via this api.","type":"string","format":"uuid"},"settings":{"description":"Free-form app-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"icon":{"description":"Single-glyph emoji shown alongside the title. Defaults to ✨.","oneOf":[{"type":"string"},{"type":"null"}]},"themes":{"description":"Theme catalog: {active, definitions}. Drives end-user theming in the canvas.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"title":{"description":"Human-readable name shown in UI. 1-200 chars.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"},"preview":{"description":"Optional preview image URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"locales":{"description":"The languages this app serves: `active` (the one it is authored in), `available` (the others it offers) and `glossary` (names a translator must not translate). Read what is translated, and run the translator, at GET /api/v1/apps/{app_id}/translations.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"}},"required":["id","org_id","slug","title","icon","archived","settings","themes","locales","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility on the org but lacks the role to create apps.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Target org does not exist, or the caller has no visibility (intentionally identical shapes).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"An app with this slug already exists in the target org.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"A language code in `locales.available` is not usable; no app was created.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"Create a new app","description":"Creates an app under `org_id`.\n\nAuthorization: the caller must have an active member row in `org_id` and a role granting `[:write :resource]` (owner, admin, or member). Viewer is rejected with 403.\n\nCross-org creation (a token writing into an org it can't see) returns 404 carrying the `org-not-found` spec, not 403 — same opacity as /orgs.\n\nSlugs are unique within an org. A slug collision returns 409 carrying the `app-slug-taken` spec; pre-flight with GET /api/v1/apps?org_id=<id> if you need to check.\n\nSupports the `Idempotency-Key` header — retry the same create without minting a second app."}},"/api/v1/workspaces/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Workspace id from GET /api/v1/apps/{app_id}/workspaces"},{"in":"query","name":"view","required":false,"schema":{"default":"full","type":"string","enum":["outline","full"]},"description":"Node detail: outline | full. Default full."},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":1000},"description":"Maximum node rows. 1-1000."},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0},"description":"Node offset. Default 0."}],"responses":{"200":{"description":"The workspace row plus its flat node list.","content":{"application/json":{"schema":{"title":"Workspace","description":"Workspace plus its flat normalized node list.","type":"object","properties":{"description":{"description":"Optional blurb.","oneOf":[{"type":"string"},{"type":"null"}]},"archived":{"description":"True once the workspace has been archived.","type":"boolean"},"limit":{"description":"The window that was applied. Equals `node_count` when the request named no limit.","type":"integer"},"offset":{"description":"The offset that was applied. 0 when the request named none.","type":"integer"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"root_node_id":{"description":"Convenience pointer to the root node. Null only when the workspace has not been seeded with a root.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"app_id":{"description":"Owning app id.","type":"string","format":"uuid"},"settings":{"description":"Free-form workspace settings map. {} when unset.","type":"object","additionalProperties":{}},"title":{"description":"Human-readable name.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp.","type":"string"},"nodes":{"description":"One window of the workspace's nodes as a flat list, sorted by (parent_id, idx, id) — the root (null parent_id) sorts first. Reconstruct the tree client-side.","type":"array","items":{"title":"Node","description":"One normalized node row. Rebuild the hierarchy from `parent_id`/`idx`.","type":"object","properties":{"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"parent_id":{"description":"Parent node id. Null for the root node of the workspace.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"idx":{"description":"Sibling ordering. 0-based, ascending.","type":"integer"},"component":{"description":"Renderer dispatch key in PascalCase (mockup nodes) or workflow node type. Maps to a library block's `component` or `slug`.","type":"string"},"props":{"description":"Per-node prop map. Schema varies by `component`; consult the library block for the declared prop shape.","type":"object","additionalProperties":{}},"hidden":{"description":"True when the node is hidden from the canvas without being archived.","type":"boolean"},"archived":{"description":"True when the node has been soft-deleted. Archived nodes still surface here for inspection.","type":"boolean"}},"required":["id","parent_id","idx","component","props","hidden","archived"],"additionalProperties":false}},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Workspace kind: `mockup` | `workflow` | `entity`.","type":"string"},"truncated":{"description":"True when nodes remain past this window — read them with `offset`.","type":"boolean"},"node_count":{"description":"Total nodes in the workspace, before the window.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp.","type":"string"}},"required":["id","org_id","app_id","kind","title","settings","archived","root_node_id","created_at","updated_at","nodes","node_count","limit","offset","truncated"],"additionalProperties":false}}}},"404":{"description":"Workspace does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Workspaces"],"summary":"Get a workspace with its nodes","description":"Returns the workspace row plus every node as a flat normalized list.\n\nEach node carries `parent_id` and `idx` — reconstruct the hierarchy client-side (group by `parent_id`, sort siblings by `idx`; the root has `parent_id: null`). The list is sorted by (parent_id, idx, id) for a deterministic order.\n\nArchived and hidden nodes are returned alongside live ones so clients can inspect history; filter on `archived`/`hidden` locally if you only want renderable subtrees.\n\nReturns 404 (`workspace-not-found`) when the id doesn't resolve OR the caller has no visibility on the workspace's org. The shape is intentionally identical for both cases."},"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Workspace id from GET /api/v1/apps/{app_id}/workspaces"}],"requestBody":{"content":{"application/json":{"schema":{"title":"WorkspacePatch","description":"Partial-update body. Send only the fields you want to change; omitted fields are preserved. Unknown fields are dropped by coercion, so protected fields (id, org_id, app_id, kind) cannot be modified here.","type":"object","properties":{"title":{"description":"New title. 1-200 chars.","allOf":[{"type":"string"},{}]},"archived":{"description":"Set true to archive the workspace, false to restore it. Archived workspaces stay readable.","type":"boolean"}},"additionalProperties":false}}}},"responses":{"200":{"description":"The updated workspace row plus its flat node list.","content":{"application/json":{"schema":{"title":"Workspace","description":"Workspace plus its flat normalized node list.","type":"object","properties":{"description":{"description":"Optional blurb.","oneOf":[{"type":"string"},{"type":"null"}]},"archived":{"description":"True once the workspace has been archived.","type":"boolean"},"limit":{"description":"The window that was applied. Equals `node_count` when the request named no limit.","type":"integer"},"offset":{"description":"The offset that was applied. 0 when the request named none.","type":"integer"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"root_node_id":{"description":"Convenience pointer to the root node. Null only when the workspace has not been seeded with a root.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"app_id":{"description":"Owning app id.","type":"string","format":"uuid"},"settings":{"description":"Free-form workspace settings map. {} when unset.","type":"object","additionalProperties":{}},"title":{"description":"Human-readable name.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp.","type":"string"},"nodes":{"description":"One window of the workspace's nodes as a flat list, sorted by (parent_id, idx, id) — the root (null parent_id) sorts first. Reconstruct the tree client-side.","type":"array","items":{"title":"Node","description":"One normalized node row. Rebuild the hierarchy from `parent_id`/`idx`.","type":"object","properties":{"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"parent_id":{"description":"Parent node id. Null for the root node of the workspace.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"idx":{"description":"Sibling ordering. 0-based, ascending.","type":"integer"},"component":{"description":"Renderer dispatch key in PascalCase (mockup nodes) or workflow node type. Maps to a library block's `component` or `slug`.","type":"string"},"props":{"description":"Per-node prop map. Schema varies by `component`; consult the library block for the declared prop shape.","type":"object","additionalProperties":{}},"hidden":{"description":"True when the node is hidden from the canvas without being archived.","type":"boolean"},"archived":{"description":"True when the node has been soft-deleted. Archived nodes still surface here for inspection.","type":"boolean"}},"required":["id","parent_id","idx","component","props","hidden","archived"],"additionalProperties":false}},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Workspace kind: `mockup` | `workflow` | `entity`.","type":"string"},"truncated":{"description":"True when nodes remain past this window — read them with `offset`.","type":"boolean"},"node_count":{"description":"Total nodes in the workspace, before the window.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp.","type":"string"}},"required":["id","org_id","app_id","kind","title","settings","archived","root_node_id","created_at","updated_at","nodes","node_count","limit","offset","truncated"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Workspace does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Workspaces"],"summary":"Update a workspace","description":"Updates `title` and/or `archived`.\n\nAuthorization: the caller must have an active member row in the workspace's org and a role granting `[:write :resource]` (owner, admin, or member). Viewer is rejected with 403.\n\nProtected fields (id, org_id, app_id, kind) are dropped by request coercion before the handler sees them. Archiving is reversible — PATCH `{\"archived\": false}` restores.\n\nReturns the same shape as GET /api/v1/workspaces/{id}, nodes included."}},"/api/v1/manifest-syncs":{"post":{"requestBody":{"content":{"application/json":{"schema":{"title":"ManifestSync","description":"Create or update every mockup, workflow, and entity a manifest declares, as Studio workspaces. The manifest becomes the source of truth for the app: anything it no longer declares is archived, which is why `allow_archive` is required the moment a run would remove something.","type":"object","properties":{"org_id":{"description":"Organization that owns the app.","type":"string","format":"uuid"},"manifest":{"description":"Parsed manifest.json object; validated against the canonical manifest schema."},"sources":{"description":"Every workspace source as a URL/content record. An array preserves path punctuation across JSON key normalization.","type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"content":{"type":"string"}},"required":["url","content"],"additionalProperties":false}},"dry_run":{"description":"Report the plan, including what would be archived, and write nothing.","type":"boolean"},"allow_archive":{"description":"Consent to archive the workspaces and nodes the manifest no longer declares.","type":"boolean"}},"required":["org_id","manifest","sources"],"additionalProperties":false}}}},"responses":{"200":{"description":"Workspace counts, the app id, and what was archived.","content":{"application/json":{"schema":{"title":"ManifestSyncResult","description":"What the sync wrote, and what it took away.","type":"object","properties":{"app_id":{"description":"The app the manifest names. Derived from `app.id`, so it is the same on every run.","type":"string","format":"uuid"},"org_id":{"description":"Organization that owns the app.","type":"string","format":"uuid"},"created":{"description":"True when this run brought the app into being.","type":"boolean"},"dry_run":{"description":"True when nothing was written.","type":"boolean"},"mockups":{"description":"Mockup workspaces the manifest declares.","type":"integer"},"workflows":{"description":"Workflow workspaces the manifest declares.","type":"integer"},"entities":{"description":"Entity sources the manifest declares.","type":"integer"},"archived":{"description":"What the manifest no longer declares, and this run therefore removed.","type":"object","properties":{"workspaces":{"type":"integer"},"nodes":{"type":"integer"}},"required":["workspaces","nodes"],"additionalProperties":false}},"required":["app_id","org_id","created","dry_run","mockups","workflows","entities","archived"],"additionalProperties":false}}}},"403":{"description":"Caller sees the org but cannot write apps in it.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Target org does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"The run would archive workspaces or nodes without `allow_archive`.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"The manifest, or one of its sources, is invalid.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"Sync a manifest bundle into Studio workspaces","description":"Creates or updates every mockup, workflow, and entity workspace a manifest declares, and archives what it no longer declares. Does not deploy the app or run startup workflows.\n\nAuthorization: the caller must have an active member row in `org_id` and a role granting `[:write :app]`.\n\nThe manifest owns the app. Send `dry_run: true` first to see the plan; a run that would archive anything is refused with 409 until you send `allow_archive: true`."}},"/api/v1/tokens":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"scopes":{"oneOf":[{"type":"array","items":{}},{"type":"null"}]},"revoked_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"expires_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"name":{"type":"string"},"org_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"last_used_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"scope":{"type":"string"},"member_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"prefix":{"type":"string"},"updated_at":{"type":"string"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"profile_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"created_at":{"type":"string"}},"required":["id","name","prefix","kind","scope","scopes","created_at","updated_at"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}}},"tags":["Tokens"],"summary":"List the caller's API tokens","description":"Returns all tokens owned by the caller. Profile-scoped tokens see their own personal tokens; org-scoped tokens see the org's tokens."},"post":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"allOf":[{"type":"string"},{}]},"kind":{"type":"string","enum":["live","test"]},"expires_at":{"description":"RFC 3339 instant in the future, e.g. 2027-01-01T00:00:00Z.","oneOf":[{"allOf":[{"type":"string"},{}]},{"type":"null"}]}},"required":["name"],"additionalProperties":false}}}},"responses":{"201":{"content":{"application/json":{"schema":{"type":"object","properties":{"scopes":{"oneOf":[{"type":"array","items":{}},{"type":"null"}]},"revoked_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"expires_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"name":{"type":"string"},"org_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"last_used_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"scope":{"type":"string"},"member_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"prefix":{"type":"string"},"updated_at":{"type":"string"},"token":{"type":"string"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"profile_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"created_at":{"type":"string"}},"required":["id","name","prefix","kind","scope","scopes","created_at","updated_at","token"],"additionalProperties":false}}}}},"tags":["Tokens"],"summary":"Create a new API token","description":"Mints a fresh token. The full plaintext is returned exactly once — store it immediately. The token's scope is inherited from the caller's identity."}},"/api/v1/deployments/{id}/domains/{hostname}":{"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."},{"in":"path","name":"hostname","required":true,"schema":{"type":"string"},"description":"The attached hostname"}],"responses":{"204":{"description":"Domain removed."},"403":{"description":"Caller lacks the role to manage domains.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Deployment or hostname not found / not visible.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Remove a custom domain","description":"Tears down the Ingress rule + certificate and drops the entry. Traffic on the hostname stops immediately.\n\nAuthorization: `[:manage :resource]` (owner/admin)."}},"/api/v1/manifest-deployments":{"post":{"requestBody":{"content":{"application/json":{"schema":{"title":"ManifestDeploymentTrigger","description":"Deploy a complete local manifest bundle without importing it into Studio workspaces.","type":"object","properties":{"org_id":{"description":"Organization that owns the app and deployment.","type":"string","format":"uuid"},"manifest":{"description":"Parsed manifest.json object; validated against the canonical manifest schema."},"sources":{"description":"Every workspace source as a URL/content record. An array preserves path punctuation across JSON key normalization.","type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"content":{"type":"string"}},"required":["url","content"],"additionalProperties":false}},"env":{"description":"Target environment. Defaults to dev — a prod launch must be asked for by name.","type":"string","enum":["dev","uat","prod"]},"tier":{"description":"Hosting tier. Defaults to free.","type":"string","enum":["free","hobby","starter","growth","scale"]},"force":{"description":"Evict another app from the org's free slot.","type":"boolean"},"plan_hash":{"description":"Approval hash for a disruptive migration plan.","type":"string"},"slug":{"description":"Vanity label to serve this app on, as <slug>.orbitapps.ai, alongside its default host.","type":"string"}},"required":["org_id","manifest","sources"],"additionalProperties":false}}}},"responses":{"202":{"description":"The deployment row at phase=building.","content":{"application/json":{"schema":{"title":"Deployment","description":"Public projection of an app deployment. One row per (app, env) pair; phase + stage transition over the row's lifetime via the studio deploy flow.","type":"object","properties":{"slug":{"description":"Vanity label the app also serves on, as <slug>.orbitapps.ai. Unique across the platform; unlike a custom domain it needs no DNS record and no certificate. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"stage":{"description":"Sub-state when phase=building: bundle | apply | wait-ready. Nil otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"issues":{"description":"What the last deploy could not build. Empty on a clean deploy. The app still serves — phase stays \"deployed\" — but the named parts are missing, so their routes and schedules are not mounted.","type":"array","items":{"title":"DeploymentIssue","description":"One part of the app the last deploy could not build.","type":"object","properties":{"kind":{"description":"What kind of thing failed to build, e.g. \"workflow\".","oneOf":[{"type":"string"},{"type":"null"}]},"workspace_id":{"description":"Workspace that holds it, so a client can link straight to the editor.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Its name, as the author sees it in Studio.","oneOf":[{"type":"string"},{"type":"null"}]},"reason":{"description":"Grep-able rule name, e.g. \"workflow-preflight-failed\".","oneOf":[{"type":"string"},{"type":"null"}]},"message":{"description":"What went wrong, already written for the author.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["kind","workspace_id","title","reason","message"],"additionalProperties":false}},"started_at":{"description":"RFC 3339 timestamp the last deploy attempt began. May be null on a never-deployed row.","oneOf":[{"type":"string"},{"type":"null"}]},"completed_at":{"description":"RFC 3339 timestamp the last deploy attempt reached a terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"events":{"description":"Per-transition timeline (sliding window of last 50). Drives the UI timeline panel.","type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"ip_address":{"description":"Provider-assigned IP, when surfaced. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"app_id":{"description":"Owning app id. Immutable: deployments cannot be reassigned to another app.","type":"string","format":"uuid"},"history":{"description":"Per-attempt deploy history (sliding window of last 20). Drives the Versions list + revert.","type":"array","items":{"title":"DeploymentHistoryEntry","description":"One per-attempt summary; powers the Versions list + Revert.","type":"object","properties":{"at":{"description":"RFC 3339 timestamp the attempt reached terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_url":{"description":"Bundle pointer in R2 — passed back via deploy to revert to this version.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_version":{"description":"Short slug from the bundle path.","oneOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"description":"Profile id (uuid string) of whoever started the attempt. Nil for reconciler-driven transitions.","oneOf":[{"type":"string"},{"type":"null"}]},"outcome":{"description":"deployed | degraded | failed | stopped. \"degraded\" means the release went live but could not build every part — see the deployment's issues.","oneOf":[{"type":"string"},{"type":"null"}]},"error_code":{"description":"Machine-readable failure code on failed/stopped outcomes; nil on deployed.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["at","bundle_url","bundle_version","triggered_by","outcome","error_code"],"additionalProperties":false}},"phase":{"description":"High-level lifecycle phase. One of: idle | building | deployed | stopping | stopped | failed.","type":"string"},"public_url":{"description":"Publicly reachable URL once the deployment is live. May be null while building / failed.","oneOf":[{"type":"string"},{"type":"null"}]},"env":{"description":"Environment slot — one of \"dev\", \"uat\", or \"prod\". Each (app, env) pair has at most one deployment row.","type":"string"},"last_heartbeat_at":{"description":"RFC 3339 timestamp of the last successful watcher/reconciler tick. Drives staleness detection.","oneOf":[{"type":"string"},{"type":"null"}]},"provider_app_id":{"description":"Identifier returned by the hosting provider after the first successful deploy.","oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"description":"RFC 3339 timestamp of the most recent transition.","type":"string"},"status":{"description":"Uniform async-job status derived from phase: pending | running | succeeded | cancelled | failed. The same enum every long-running resource exposes — poll generically on this.","type":"string"},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"bundle_url":{"description":"R2 manifest URL of the bundle currently deployed. Used by revert.","oneOf":[{"type":"string"},{"type":"null"}]},"error":{"description":"Structured failure context (source / code / detail / at). Non-null only when phase=failed.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"bundle_version":{"description":"Short slug for the bundle currently deployed.","oneOf":[{"type":"string"},{"type":"null"}]},"domain":{"description":"Custom domain bound to this deployment, when configured. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"version":{"description":"Monotonic counter, bumped on every transition. Useful as a cache-invalidation key.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp. Stable across phase transitions.","type":"string"}},"required":["id","app_id","org_id","env","phase","status","issues","events","history","version","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller cannot create and deploy apps in the org.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"The organization is not visible to the caller.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Credits, free-slot, app identity, or plan approval conflict.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"The manifest, a source, or its migration plan is invalid.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Deploy a manifest bundle","description":"Validate and deploy a complete manifest.json plus its referenced JSONL sources without importing or converting them into Studio workspaces. The bundle is preserved as authored and copied into canonical versioned storage before the existing durable hosting queue takes over. Supports Idempotency-Key."}},"/api/v1/recipes":{"get":{"parameters":[{"in":"query","name":"q","required":false,"schema":{"type":"string"},"description":"Natural-language BM25 query, e.g. `\"hero\"`, `\"pricing page\"`. Omit for plain browse + filter."},{"in":"query","name":"kind","required":false,"schema":{"type":"string","enum":["section","page","example","template"]},"description":"Recipe kind. `section` and `page` are the composable patterns; `example` is a single-block variant; `template` is a whole-app starting point."},{"in":"query","name":"family","required":false,"schema":{"type":"string"},"description":"Restrict to one variant family, e.g. `header`."},{"in":"query","name":"category","required":false,"schema":{"type":"string"},"description":"Restrict to a broader bucket, e.g. `marketing`."},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Max rows returned. 1–100, default 20."},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0},"description":"Result offset for pagination. Default 0."}],"responses":{"200":{"description":"Matching recipes in the slim search shape.","content":{"application/json":{"schema":{"title":"RecipeList","description":"List envelope. `limit` and `offset` echo the query; v1 omits a total count — detect end-of-results when `data.length < limit`.","type":"object","properties":{"data":{"type":"array","items":{"title":"RecipeMatch","description":"Slim search hit. Read the full recipe via GET /recipes/{slug}.","type":"object","properties":{"slug":{"description":"URL-safe handle, unique within its kind. The path param on GET /recipes/{slug}.","type":"string"},"kind":{"description":"Recipe kind.","type":"string"},"title":{"description":"Human-readable display name.","type":"string"},"description":{"description":"One-line summary for tooltips and ranking.","type":"string"},"family":{"description":"Variant family, e.g. `header` for all header variants.","type":"string"}},"required":["slug","kind","title","description"],"additionalProperties":false}},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["data","limit","offset"],"additionalProperties":false}}}}},"tags":["Recipes"],"summary":"Search and browse reusable recipes","description":"Returns the recipes the caller can reach.\n\nSearch semantics:\n  • Pass `q` for BM25 full-text ranking across title, slug, description, and keywords.\n  • Omit `q` to plain-browse with `kind`/`family`/`category` filters.\n  • Combine `q` with filters to scope the search.\n\nPagination is offset-based; the response omits a total count. Detect end-of-results when `data.length < limit`.\n\nRead a recipe's `composition` and `blocks` via GET /recipes/{slug}; fetch each block's spec via GET /blocks/{slug}."}},"/api/v1/deployments/{id}/slug":{"put":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"slug":{"description":"Vanity label the app is served on, as <slug>.orbitapps.ai. One lowercase DNS label: letters, digits and inner dashes, 63 chars max.","type":"string"}},"required":["slug"],"additionalProperties":false}}}},"responses":{"200":{"description":"The deployment, carrying its new slug.","content":{"application/json":{"schema":{"title":"Deployment","description":"Public projection of an app deployment. One row per (app, env) pair; phase + stage transition over the row's lifetime via the studio deploy flow.","type":"object","properties":{"slug":{"description":"Vanity label the app also serves on, as <slug>.orbitapps.ai. Unique across the platform; unlike a custom domain it needs no DNS record and no certificate. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"stage":{"description":"Sub-state when phase=building: bundle | apply | wait-ready. Nil otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"issues":{"description":"What the last deploy could not build. Empty on a clean deploy. The app still serves — phase stays \"deployed\" — but the named parts are missing, so their routes and schedules are not mounted.","type":"array","items":{"title":"DeploymentIssue","description":"One part of the app the last deploy could not build.","type":"object","properties":{"kind":{"description":"What kind of thing failed to build, e.g. \"workflow\".","oneOf":[{"type":"string"},{"type":"null"}]},"workspace_id":{"description":"Workspace that holds it, so a client can link straight to the editor.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Its name, as the author sees it in Studio.","oneOf":[{"type":"string"},{"type":"null"}]},"reason":{"description":"Grep-able rule name, e.g. \"workflow-preflight-failed\".","oneOf":[{"type":"string"},{"type":"null"}]},"message":{"description":"What went wrong, already written for the author.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["kind","workspace_id","title","reason","message"],"additionalProperties":false}},"started_at":{"description":"RFC 3339 timestamp the last deploy attempt began. May be null on a never-deployed row.","oneOf":[{"type":"string"},{"type":"null"}]},"completed_at":{"description":"RFC 3339 timestamp the last deploy attempt reached a terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"events":{"description":"Per-transition timeline (sliding window of last 50). Drives the UI timeline panel.","type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"ip_address":{"description":"Provider-assigned IP, when surfaced. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"app_id":{"description":"Owning app id. Immutable: deployments cannot be reassigned to another app.","type":"string","format":"uuid"},"history":{"description":"Per-attempt deploy history (sliding window of last 20). Drives the Versions list + revert.","type":"array","items":{"title":"DeploymentHistoryEntry","description":"One per-attempt summary; powers the Versions list + Revert.","type":"object","properties":{"at":{"description":"RFC 3339 timestamp the attempt reached terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_url":{"description":"Bundle pointer in R2 — passed back via deploy to revert to this version.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_version":{"description":"Short slug from the bundle path.","oneOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"description":"Profile id (uuid string) of whoever started the attempt. Nil for reconciler-driven transitions.","oneOf":[{"type":"string"},{"type":"null"}]},"outcome":{"description":"deployed | degraded | failed | stopped. \"degraded\" means the release went live but could not build every part — see the deployment's issues.","oneOf":[{"type":"string"},{"type":"null"}]},"error_code":{"description":"Machine-readable failure code on failed/stopped outcomes; nil on deployed.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["at","bundle_url","bundle_version","triggered_by","outcome","error_code"],"additionalProperties":false}},"phase":{"description":"High-level lifecycle phase. One of: idle | building | deployed | stopping | stopped | failed.","type":"string"},"public_url":{"description":"Publicly reachable URL once the deployment is live. May be null while building / failed.","oneOf":[{"type":"string"},{"type":"null"}]},"env":{"description":"Environment slot — one of \"dev\", \"uat\", or \"prod\". Each (app, env) pair has at most one deployment row.","type":"string"},"last_heartbeat_at":{"description":"RFC 3339 timestamp of the last successful watcher/reconciler tick. Drives staleness detection.","oneOf":[{"type":"string"},{"type":"null"}]},"provider_app_id":{"description":"Identifier returned by the hosting provider after the first successful deploy.","oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"description":"RFC 3339 timestamp of the most recent transition.","type":"string"},"status":{"description":"Uniform async-job status derived from phase: pending | running | succeeded | cancelled | failed. The same enum every long-running resource exposes — poll generically on this.","type":"string"},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"bundle_url":{"description":"R2 manifest URL of the bundle currently deployed. Used by revert.","oneOf":[{"type":"string"},{"type":"null"}]},"error":{"description":"Structured failure context (source / code / detail / at). Non-null only when phase=failed.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"bundle_version":{"description":"Short slug for the bundle currently deployed.","oneOf":[{"type":"string"},{"type":"null"}]},"domain":{"description":"Custom domain bound to this deployment, when configured. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"version":{"description":"Monotonic counter, bumped on every transition. Useful as a cache-invalidation key.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp. Stable across phase transitions.","type":"string"}},"required":["id","app_id","org_id","env","phase","status","issues","events","history","version","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller lacks the role to manage domains.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"That slug is already in use by another deployment.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"The slug is not a valid label, or it is reserved.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Set the deployment's vanity address","description":"Serves the app at `<slug>.orbitapps.ai` in addition to its default host — both addresses stay live.\n\nUnlike a custom domain this needs no DNS record and no certificate: the platform wildcard already covers it, so there is nothing to verify. When the app is already deployed the route goes live immediately, with no redeploy.\n\nSlugs are unique across the platform.\n\nAuthorization: `[:manage :resource]` (owner/admin) — a vanity address routes live traffic."},"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."}],"responses":{"204":{"description":"Slug cleared."},"403":{"description":"Caller lacks the role to manage domains.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Clear the deployment's vanity address","description":"Stops serving the app on `<slug>.orbitapps.ai` and frees the name for reuse. The default host is unaffected.\n\nAuthorization: `[:manage :resource]` (owner/admin)."}},"/api/v1/orgs/{id}/members/{member_id}":{"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"},{"in":"path","name":"member_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Member id from GET /api/v1/orgs/{id}/members"}],"responses":{"204":{"description":"Member removed."},"403":{"description":"Caller lacks the role to manage members.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Org or member not found / not visible.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Owner-protected, or the caller targeted themselves.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Members"],"summary":"Remove a member","description":"Hard-deletes the membership row. Pending invites can be revoked the same way. The owner is protected, and callers cannot remove themselves (leave the org from Studio instead).\n\nAuthorization: `[:manage :member]` (owner/admin)."},"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"},{"in":"path","name":"member_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Member id from GET /api/v1/orgs/{id}/members"}],"requestBody":{"content":{"application/json":{"schema":{"title":"MemberRolePatch","type":"object","properties":{"role":{"description":"Assignable roles. `owner` is reserved — ownership transfer is a separate studio flow.","type":"string","enum":["admin","member","viewer"]}},"required":["role"],"additionalProperties":false}}}},"responses":{"200":{"description":"The updated member row.","content":{"application/json":{"schema":{"title":"Member","description":"One org membership row.","type":"object","properties":{"role":{"description":"owner | admin | member | viewer.","type":"string"},"email":{"description":"The membership's email. Null for a non-human member — an agent has no inbox.","oneOf":[{"type":"string"},{"type":"null"}]},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"invited | active | suspended.","type":"string"},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"human | agent | integration | runner. Rows written before kinds existed read as `human`.","type":"string"},"joined_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"profile_id":{"description":"Linked profile once the invite is accepted. Null while invited.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"invited_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","profile_id","kind","email","role","status"],"additionalProperties":false}}}},"403":{"description":"Caller lacks the role to manage members.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Org or member not found / not visible.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Owner-protected, or the caller targeted themselves.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Members"],"summary":"Change a member's role","description":"Sets the member's role to `admin`, `member`, or `viewer`. The owner's row is protected (409 `member-protected`), and callers cannot change their own role (409 `member-self`).\n\nAuthorization: `[:manage :member]` (owner/admin)."}},"/api/v1/artifacts/{id}/content":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Artifact id."}],"responses":{"200":{"description":"The bytes."},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Artifacts"],"summary":"Get an artifact's bytes","description":"The file itself. A public artifact answers without a token — this is the address a deployed page renders from. A private one needs the org-scoped token that owns it.","security":[{},{"bearerAuth":[]}]}},"/api/v1/apps/{id}/duplicate":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"}],"requestBody":{"content":{"application/json":{"schema":{"title":"AppDuplicate","description":"Request body for POST /api/v1/apps/{id}/duplicate. Everything is optional — the default clone is titled \"<source> (Copy)\".","type":"object","properties":{"title":{"description":"Title for the clone (also seeds its slug). Defaults to \"<source title> (Copy)\".","allOf":[{"type":"string"},{}]}},"additionalProperties":false}}}},"responses":{"201":{"description":"The cloned app row. List its workspaces with GET /api/v1/apps/{id}/workspaces.","content":{"application/json":{"schema":{"title":"App","description":"Public projection of an app the caller can see. Mutable fields are exposed via PATCH /api/v1/apps/{id}.","type":"object","properties":{"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"idea_id":{"description":"Linked idea id, when the app was scaffolded from an idea. May be null.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"archived":{"description":"True once the app has been archived. Archive/delete verbs are not yet exposed in v1.","type":"boolean"},"slug":{"description":"URL-safe identifier, unique within the org. Lowercase letters, digits, and hyphens; 1-60 chars.","type":"string"},"org_id":{"description":"Owning org id. Immutable: apps cannot be transferred between orgs via this api.","type":"string","format":"uuid"},"settings":{"description":"Free-form app-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"icon":{"description":"Single-glyph emoji shown alongside the title. Defaults to ✨.","oneOf":[{"type":"string"},{"type":"null"}]},"themes":{"description":"Theme catalog: {active, definitions}. Drives end-user theming in the canvas.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"title":{"description":"Human-readable name shown in UI. 1-200 chars.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"},"preview":{"description":"Optional preview image URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"locales":{"description":"The languages this app serves: `active` (the one it is authored in), `available` (the others it offers) and `glossary` (names a translator must not translate). Read what is translated, and run the translator, at GET /api/v1/apps/{app_id}/translations.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"}},"required":["id","org_id","slug","title","icon","archived","settings","themes","locales","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"Duplicate an app","description":"Clones the app — metadata, every workspace, and every node — into a fresh app in the same org. Node ids are re-minted; parent references, workflow edge endpoints, and `:inputs` data-flow refs are remapped onto the clone, so the copy is fully independent of the source.\n\nThe clone is titled \"<source> (Copy)\" unless the body provides a `title`; the slug derives from the title plus a unique suffix, so duplicates never collide.\n\nAuthorization: `[:write :resource]` (owner/admin/member) on the app's org. 404 opacity for non-members.\n\nSupports the `Idempotency-Key` header — retry the same duplicate without minting a second clone."}},"/api/v1/deployments/{id}/logs":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."},{"in":"query","name":"tail","required":false,"schema":{"type":"integer","minimum":1,"maximum":1000},"description":"Max number of log lines to return. 1-1000."},{"in":"query","name":"since","required":false,"schema":{"allOf":[{"type":"string"},{}]},"description":"Duration like '30s', '5m', or '1h' (bare digits = seconds). Returns logs from the last N units."}],"responses":{"200":{"description":"Recent log lines from the deployment. May include a soft `error` warning.","content":{"application/json":{"schema":{"title":"DeploymentLogs","description":"Recent runtime logs for the deployment. The `error` field is a SOFT warning from the hosting layer (e.g. 'logs truncated', 'provider partial response'); when present, `logs` may still carry some entries. The call itself always succeeds with 200.","type":"object","properties":{"logs":{"type":"array","items":{"title":"LogLine","description":"A single line from the deployment's runtime logs.","type":"object","properties":{"ts":{"description":"RFC 3339 timestamp.","type":"string"},"level":{"description":"Severity tag (info / warn / error / debug). Best-effort — providers vary in what they emit.","type":"string"},"line":{"description":"Log message body.","type":"string"}},"required":["ts","level","line"],"additionalProperties":false}},"error":{"description":"Provider-side warning if logs couldn't be fully fetched; null on a clean success.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["logs","error"],"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Deployment has no provider_app_id — never successfully provisioned.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Fetch recent runtime logs","description":"Returns the deployment's recent runtime logs, fetched live from the configured hosting provider.\n\nThe response is always 200 with `{logs, error}`. The `error` field is a SOFT warning from the hosting layer (e.g. 'logs truncated', 'provider partial response') — when present, `logs` may still carry some entries. Inspect the field for partial-failure context; treat non-empty `error` + empty `logs` as 'logs unavailable'.\n\nAuthorization: any active member of the deployment's org. Logs are read-only — viewer role IS allowed, unlike /restart and /stop.\n\nReturns 409 `deployment-not-provisioned` when the deployment has no `provider_app_id` — there is no provider object to fetch logs from yet."}},"/api/v1/workspaces/{id}/nodes":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Workspace id from GET /api/v1/apps/{app_id}/workspaces"}],"requestBody":{"content":{"application/json":{"schema":{"title":"NodeBatch","description":"Atomic batch of node upserts and removes. Preflight validates the whole batch first; one bad op fails everything with 422 + `issues` and nothing commits.","type":"object","properties":{"upserts":{"description":"Rows to create or update, applied in order.","type":"array","items":{"title":"NodeUpsert","description":"One node row to create or update. `id` is either the UUID of an existing node (partial update — only the provided fields change) or a temp-id label matching ^[a-z][a-z0-9_]{0,63}$ that mints a new node; the response maps labels to minted UUIDs. New nodes require `parent_id` and `component`.","type":"object","properties":{"id":{"description":"Existing node UUID, or a temp-id label for a new node. Labels may be referenced by `parent_id` anywhere in the same batch.","type":"string"},"parent_id":{"description":"Parent node: a UUID already in the workspace, or a label minted in this batch. Only the root has a null parent — it cannot be re-parented.","oneOf":[{"type":"string"},{"type":"null"}]},"idx":{"description":"Sibling position, 0-based. The caller owns ordering: inserting mid-list means also sending the displaced siblings' new idx. Defaults to 0 for new nodes.","type":"integer"},"component":{"description":"Renderer dispatch key (see GET /api/v1/blocks). Required for new nodes.","allOf":[{"type":"string"},{}]},"props":{"description":"Per-node prop map. Replaces the node's props wholesale — merge client-side before sending.","type":"object","additionalProperties":{}},"hidden":{"description":"Hide from the canvas without archiving.","type":"boolean"},"archived":{"description":"Set false to restore a node a previous batch removed (descendants must be restored explicitly).","type":"boolean"}},"required":["id"],"additionalProperties":false}},"removes":{"description":"Node UUIDs to remove. Removal archives the node and its descendants (reversible via upsert).","type":"array","items":{"type":"string","format":"uuid"}},"dry_run":{"description":"When true, runs preflight only — returns `issues` (if any) without committing or minting ids.","type":"boolean"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Batch outcome: committed (`applied: true`) or a clean dry run.","content":{"application/json":{"schema":{"title":"NodeBatchResult","description":"Outcome of a node batch.","type":"object","properties":{"applied":{"description":"True when the batch committed. False on dry runs.","type":"boolean"},"node_ids":{"description":"Temp-id label → minted node UUID, for every label in the batch. Empty on dry runs.","type":"object","additionalProperties":{"type":"string","format":"uuid"}},"issues":{"description":"Preflight failures. Always empty on 200 — failures surface as 422 with this same array in the problem body.","type":"array","items":{"title":"BatchIssue","description":"One preflight failure. `in` + `index` locate the offending entry in the request.","type":"object","properties":{"in":{"description":"Which request array the issue points into: `upserts` or `removes`.","type":"string"},"index":{"description":"0-based position within that array.","type":"integer"},"kind":{"description":"Stable machine-readable failure kind: `invalid-label`, `duplicate-label`, `duplicate-id`, `unknown-node`, `unknown-parent`, `missing-parent`, `missing-component`, `unknown-component` (the `component` names nothing this workspace kind can render — see GET /api/v1/blocks), `null-parent`, `root-parent-change`, `root-remove`, or `cycle`.","type":"string"},"reason":{"description":"Human-readable explanation. For `unknown-component` this suggests the nearest valid component name when there is one.","type":"string"},"ref":{"description":"The id/label the issue is about.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["in","index","kind","reason","ref"],"additionalProperties":false}},"warnings":{"description":"Non-fatal advisories about authored classes (unknown/off-theme). The batch applies regardless. Empty when every class is clean. See GET /api/v1/apps/{app_id}/theme for the app's valid vocabulary.","type":"array","items":{"title":"BatchWarning","description":"A non-fatal advisory about an authored `class` prop. Warnings never block a commit — the batch still applies.","type":"object","properties":{"kind":{"description":"Stable machine-readable kind: `unknown-utility` (the class emits no CSS — likely a typo or nonexistent utility/color) or `off-theme-color` (renders, but uses a raw Tailwind palette instead of a theme token).","type":"string"},"class":{"description":"The flagged class token.","type":"string"},"indices":{"description":"0-based positions in `upserts` whose `class` prop contains this token.","type":"array","items":{"type":"integer"}},"reason":{"description":"Human-readable explanation and suggested fix.","type":"string"}},"required":["kind","class","indices","reason"],"additionalProperties":false}}},"required":["applied","node_ids","issues","warnings"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Workspace does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"Preflight failed; the problem body carries the `issues` array and nothing was applied.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Workspaces"],"summary":"Apply an atomic node batch","description":"The authoring primitive: declare the node rows you want (`upserts`) and the subtrees you no longer want (`removes`); the whole batch preflights first and commits in one transaction — or fails as a unit with 422 + `issues`.\n\nRows use the SAME shape GET /api/v1/workspaces/{id} returns. New nodes use a temp-id label (^[a-z][a-z0-9_]{0,63}$) as `id` and may be referenced by `parent_id` anywhere in the batch; the response's `node_ids` maps each label to its minted UUID. Existing nodes are addressed by UUID and partially updated — only provided fields change; `props` replaces wholesale.\n\nThe caller owns sibling order: inserting mid-list means also upserting the displaced siblings' `idx`. Moving = changing `parent_id`/`idx`. Removing archives the node and its descendants; archived rows still surface on GET and a later batch can restore them with `archived: false`.\n\n`dry_run: true` runs preflight only — nothing commits, no ids mint.\n\nRetries: send an `Idempotency-Key` header (any unique string per logical batch). An identical retry within 24h replays the stored response — marked by an `Idempotency-Replayed: true` response header — instead of minting duplicate nodes; the same key with a different body returns 409 (`idempotency-key-reuse`).\n\nAuthorization: `[:write :resource]` (owner/admin/member); 404 opacity for non-members."}},"/api/v1/executions/{id}/cancel":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Execution id from POST /api/v1/workspaces/{id}/executions"}],"responses":{"200":{"description":"The execution after the cancel (or its terminal state when already finished).","content":{"application/json":{"schema":{"title":"Execution","description":"One workflow run — the uniform async-job shape. Poll until `status` turns terminal, or tail /events.","type":"object","properties":{"workspace_id":{"description":"The workflow workspace this run executed.","type":"string","format":"uuid"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"app_id":{"description":"Owning app id.","type":"string","format":"uuid"},"updated_at":{"description":"RFC 3339 timestamp.","oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"running | succeeded | failed | cancelled.","type":"string"},"result":{"description":"The workflow's result once status=succeeded. Null while running."},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"error":{"description":"Structured failure ({node_id, message}) once status=failed.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"seq":{"description":"Latest event's seq — resume /events with ?after_seq= from here.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","app_id","workspace_id","status","seq","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to cancel.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Execution does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"The run is live on another instance — it cannot be stopped from here.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Executions"],"summary":"Cancel a running execution","description":"Stops the run and persists `status=cancelled`. Cancelling an already-finished run is idempotent — 200 with the run's real outcome.\n\nCancel is process-local: a running row whose handle lives on another api instance (or was orphaned by a restart) answers 409 `execution-not-cancellable`.\n\nAuthorization: `[:write :resource]` (owner/admin/member)."}},"/api/v1/apps/{id}/unarchive":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"}],"responses":{"200":{"description":"The app row with `archived = false`.","content":{"application/json":{"schema":{"title":"App","description":"Public projection of an app the caller can see. Mutable fields are exposed via PATCH /api/v1/apps/{id}.","type":"object","properties":{"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"idea_id":{"description":"Linked idea id, when the app was scaffolded from an idea. May be null.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"archived":{"description":"True once the app has been archived. Archive/delete verbs are not yet exposed in v1.","type":"boolean"},"slug":{"description":"URL-safe identifier, unique within the org. Lowercase letters, digits, and hyphens; 1-60 chars.","type":"string"},"org_id":{"description":"Owning org id. Immutable: apps cannot be transferred between orgs via this api.","type":"string","format":"uuid"},"settings":{"description":"Free-form app-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"icon":{"description":"Single-glyph emoji shown alongside the title. Defaults to ✨.","oneOf":[{"type":"string"},{"type":"null"}]},"themes":{"description":"Theme catalog: {active, definitions}. Drives end-user theming in the canvas.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"title":{"description":"Human-readable name shown in UI. 1-200 chars.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"},"preview":{"description":"Optional preview image URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"locales":{"description":"The languages this app serves: `active` (the one it is authored in), `available` (the others it offers) and `glossary` (names a translator must not translate). Read what is translated, and run the translator, at GET /api/v1/apps/{app_id}/translations.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"}},"required":["id","org_id","slug","title","icon","archived","settings","themes","locales","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"Unarchive an app","description":"Restores a previously archived app: flips `archived` back to false.\n\nAuthorization: `[:write :resource]` (owner/admin/member) on the app's org. 404 opacity for non-members."}},"/api/v1/orgs/{id}/usage":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"}],"responses":{"200":{"description":"Per-day usage, sorted by day ascending.","content":{"application/json":{"schema":{"title":"UsageSeries","type":"object","properties":{"data":{"type":"array","items":{"title":"UsageDay","description":"One day's usage cost (cents) split by resource type.","type":"object","properties":{"day":{"description":"YYYY-MM-DD.","type":"string"},"by_type":{"description":"resource-type → cents (e.g. hosting_uptime, ai_tokens).","type":"object","additionalProperties":{}},"total":{"description":"Day total, cents."}},"required":["day","by_type","total"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Org does not exist, or the caller has no visibility (intentionally identical shapes).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Billing"],"summary":"Get the org's per-day usage","description":"Daily cost series (cents) split by resource type — hosting uptime and AI tokens. Every meter counts (open, closed, settled) so history is complete.\n\nAuthorization: any active member (viewer included)."}},"/api/v1/device/token":{"post":{"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"oneOf":[{"title":"OAuthDeviceTokenRequest","type":"object","properties":{"grant_type":{"type":"string"},"device_code":{"type":"string"},"client_id":{"type":"string","minLength":1,"maxLength":64}},"additionalProperties":false},{"type":"null"}]}}}},"responses":{"200":{"description":"The minted token — shown exactly once. Manage it afterwards via /api/v1/tokens/{id}.","content":{"application/json":{"schema":{"anyOf":[{"title":"DeviceTokenResponse","description":"The minted api token — shown exactly once, exactly here.","type":"object","properties":{"token":{"description":"The bearer token plaintext. Store it securely; it is not retrievable again.","type":"string"},"token_id":{"description":"The token row's id — manage it via /api/v1/tokens/{id}.","type":"string","format":"uuid"},"scope":{"description":"profile | org, from the approval.","type":"string"},"kind":{"description":"Always `live`.","type":"string"}},"required":["token","token_id","scope","kind"],"additionalProperties":false},{"title":"OAuthDeviceTokenResponse","type":"object","properties":{"access_token":{"type":"string"},"token_type":{"const":"Bearer"},"scope":{"type":"string"}},"required":["access_token","token_type","scope"],"additionalProperties":false}]}}}},"400":{"description":"RFC 8628 flow state: authorization_pending | slow_down | access_denied | expired_token | invalid_grant.","content":{"application/json":{"schema":{"title":"DeviceFlowError","description":"RFC 8628 error vocabulary: authorization_pending (keep polling), slow_down (poll slower), access_denied (user denied), expired_token (grant expired or already claimed — restart the flow), invalid_grant (unknown/forged device_code).","type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}},"tags":["Device"],"summary":"Poll for the minted token","description":"Polls the grant. While the user hasn't acted: 400 `authorization_pending` — keep polling every `interval` seconds (polling faster earns `slow_down`). Once approved, THIS call mints the api token and returns it — exactly once; the grant is then `claimed` and further polls answer `expired_token`.\n\nTerminal errors: `access_denied` (user declined), `expired_token` (grant older than 15 minutes or already claimed — restart the flow), `invalid_grant` (unknown/forged device_code).\n\nErrors use RFC 8628's bare `{error}` vocabulary, not problem+json, so standard device-flow clients work unmodified.","security":[]}},"/api/v1/workspaces/{id}/recipes":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Workspace id from GET /api/v1/apps/{app_id}/workspaces"}],"requestBody":{"content":{"application/json":{"schema":{"title":"RecipeApply","description":"Insert a catalog recipe's composition into this workspace as real nodes.","type":"object","properties":{"slug":{"description":"Recipe slug (see GET /api/v1/recipes).","type":"string"},"parent_id":{"description":"Node the recipe attaches under. Defaults to the workspace root.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"dry_run":{"description":"When true, runs preflight only — nothing commits and no ids mint.","type":"boolean"}},"required":["slug"],"additionalProperties":false}}}},"responses":{"200":{"description":"The recipe's nodes were inserted, or a clean dry run.","content":{"application/json":{"schema":{"title":"RecipeApplyResult","description":"Outcome of inserting a recipe, mirroring the node-batch result.","type":"object","properties":{"applied":{"description":"True when the nodes committed. False on a dry run.","type":"boolean"},"slug":{"description":"The recipe that was applied.","type":"string"},"kind":{"description":"The recipe's kind. A `page` recipe grafts its children under the target and leaves the target's own props alone; any other kind inserts whole.","type":"string"},"node_ids":{"description":"Temp label → minted node UUID, one per inserted node. Empty on a dry run.","type":"object","additionalProperties":{"type":"string","format":"uuid"}},"issues":{"description":"Preflight failures. Always empty on 200 — failures surface as 422 with this same array in the problem body.","type":"array","items":{"title":"BatchIssue","description":"One preflight failure. `in` + `index` locate the offending entry in the request.","type":"object","properties":{"in":{"description":"Which request array the issue points into: `upserts` or `removes`.","type":"string"},"index":{"description":"0-based position within that array.","type":"integer"},"kind":{"description":"Stable machine-readable failure kind: `invalid-label`, `duplicate-label`, `duplicate-id`, `unknown-node`, `unknown-parent`, `missing-parent`, `missing-component`, `unknown-component` (the `component` names nothing this workspace kind can render — see GET /api/v1/blocks), `null-parent`, `root-parent-change`, `root-remove`, or `cycle`.","type":"string"},"reason":{"description":"Human-readable explanation. For `unknown-component` this suggests the nearest valid component name when there is one.","type":"string"},"ref":{"description":"The id/label the issue is about.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["in","index","kind","reason","ref"],"additionalProperties":false}},"warnings":{"description":"Non-fatal advisories about the recipe's authored classes. Never blocks the insert.","type":"array","items":{"title":"BatchWarning","description":"A non-fatal advisory about an authored `class` prop. Warnings never block a commit — the batch still applies.","type":"object","properties":{"kind":{"description":"Stable machine-readable kind: `unknown-utility` (the class emits no CSS — likely a typo or nonexistent utility/color) or `off-theme-color` (renders, but uses a raw Tailwind palette instead of a theme token).","type":"string"},"class":{"description":"The flagged class token.","type":"string"},"indices":{"description":"0-based positions in `upserts` whose `class` prop contains this token.","type":"array","items":{"type":"integer"}},"reason":{"description":"Human-readable explanation and suggested fix.","type":"string"}},"required":["kind","class","indices","reason"],"additionalProperties":false}}},"required":["applied","slug","kind","node_ids","issues","warnings"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Workspace or recipe does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"The recipe has no composition, parent_id is not in this workspace, or the nodes failed preflight.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Workspaces"],"summary":"Apply a recipe into this workspace","description":"Inserts a catalog recipe's composition as real nodes, in one call. The alternative is reading the composition from GET /api/v1/recipes/{slug} and hand-flattening its tree into a node batch; this does that server-side.\n\nA recipe's composition carries no ids, so every node is labelled and minted here — `node_ids` maps each label to its UUID, exactly as the node-batch endpoint does.\n\n`parent_id` is the node the recipe attaches under, defaulting to the workspace root. The recipe's top level appends after whatever that parent already holds, so applying two section recipes in sequence stacks them rather than overlapping.\n\nKind matters: a `page` recipe roots at a Page node and a mockup workspace already has one, so its children graft onto the target and the workspace keeps its own slug, title and visibility. Every other kind inserts whole.\n\n`dry_run: true` runs preflight only — nothing commits, no ids mint. Retries take an `Idempotency-Key` header, same contract as the node batch.\n\nAuthorization: `[:write :resource]` (owner/admin/member); 404 opacity for non-members."}},"/api/v1/deployments/{id}/stop":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."}],"responses":{"200":{"description":"The deployment row after the stop, with `phase = stopped`.","content":{"application/json":{"schema":{"title":"Deployment","description":"Public projection of an app deployment. One row per (app, env) pair; phase + stage transition over the row's lifetime via the studio deploy flow.","type":"object","properties":{"slug":{"description":"Vanity label the app also serves on, as <slug>.orbitapps.ai. Unique across the platform; unlike a custom domain it needs no DNS record and no certificate. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"stage":{"description":"Sub-state when phase=building: bundle | apply | wait-ready. Nil otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"issues":{"description":"What the last deploy could not build. Empty on a clean deploy. The app still serves — phase stays \"deployed\" — but the named parts are missing, so their routes and schedules are not mounted.","type":"array","items":{"title":"DeploymentIssue","description":"One part of the app the last deploy could not build.","type":"object","properties":{"kind":{"description":"What kind of thing failed to build, e.g. \"workflow\".","oneOf":[{"type":"string"},{"type":"null"}]},"workspace_id":{"description":"Workspace that holds it, so a client can link straight to the editor.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Its name, as the author sees it in Studio.","oneOf":[{"type":"string"},{"type":"null"}]},"reason":{"description":"Grep-able rule name, e.g. \"workflow-preflight-failed\".","oneOf":[{"type":"string"},{"type":"null"}]},"message":{"description":"What went wrong, already written for the author.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["kind","workspace_id","title","reason","message"],"additionalProperties":false}},"started_at":{"description":"RFC 3339 timestamp the last deploy attempt began. May be null on a never-deployed row.","oneOf":[{"type":"string"},{"type":"null"}]},"completed_at":{"description":"RFC 3339 timestamp the last deploy attempt reached a terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"events":{"description":"Per-transition timeline (sliding window of last 50). Drives the UI timeline panel.","type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"ip_address":{"description":"Provider-assigned IP, when surfaced. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"app_id":{"description":"Owning app id. Immutable: deployments cannot be reassigned to another app.","type":"string","format":"uuid"},"history":{"description":"Per-attempt deploy history (sliding window of last 20). Drives the Versions list + revert.","type":"array","items":{"title":"DeploymentHistoryEntry","description":"One per-attempt summary; powers the Versions list + Revert.","type":"object","properties":{"at":{"description":"RFC 3339 timestamp the attempt reached terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_url":{"description":"Bundle pointer in R2 — passed back via deploy to revert to this version.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_version":{"description":"Short slug from the bundle path.","oneOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"description":"Profile id (uuid string) of whoever started the attempt. Nil for reconciler-driven transitions.","oneOf":[{"type":"string"},{"type":"null"}]},"outcome":{"description":"deployed | degraded | failed | stopped. \"degraded\" means the release went live but could not build every part — see the deployment's issues.","oneOf":[{"type":"string"},{"type":"null"}]},"error_code":{"description":"Machine-readable failure code on failed/stopped outcomes; nil on deployed.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["at","bundle_url","bundle_version","triggered_by","outcome","error_code"],"additionalProperties":false}},"phase":{"description":"High-level lifecycle phase. One of: idle | building | deployed | stopping | stopped | failed.","type":"string"},"public_url":{"description":"Publicly reachable URL once the deployment is live. May be null while building / failed.","oneOf":[{"type":"string"},{"type":"null"}]},"env":{"description":"Environment slot — one of \"dev\", \"uat\", or \"prod\". Each (app, env) pair has at most one deployment row.","type":"string"},"last_heartbeat_at":{"description":"RFC 3339 timestamp of the last successful watcher/reconciler tick. Drives staleness detection.","oneOf":[{"type":"string"},{"type":"null"}]},"provider_app_id":{"description":"Identifier returned by the hosting provider after the first successful deploy.","oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"description":"RFC 3339 timestamp of the most recent transition.","type":"string"},"status":{"description":"Uniform async-job status derived from phase: pending | running | succeeded | cancelled | failed. The same enum every long-running resource exposes — poll generically on this.","type":"string"},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"bundle_url":{"description":"R2 manifest URL of the bundle currently deployed. Used by revert.","oneOf":[{"type":"string"},{"type":"null"}]},"error":{"description":"Structured failure context (source / code / detail / at). Non-null only when phase=failed.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"bundle_version":{"description":"Short slug for the bundle currently deployed.","oneOf":[{"type":"string"},{"type":"null"}]},"domain":{"description":"Custom domain bound to this deployment, when configured. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"version":{"description":"Monotonic counter, bumped on every transition. Useful as a cache-invalidation key.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp. Stable across phase transitions.","type":"string"}},"required":["id","app_id","org_id","env","phase","status","issues","events","history","version","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility on the deployment but lacks the role to mutate it.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Deployment has no provider_app_id — never successfully provisioned.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Stop a deployment","description":"Destroys the active deployment via the configured hosting provider and marks the row `stopped`. The request body is empty — the deployment to stop is fully identified by the path.\n\nAuthorization: the caller must be an active member of the deployment's org with a role granting `[:write :resource]` (owner, admin, or member). Viewer is rejected with 403 carrying the `insufficient-role` spec.\n\nReturns 409 `deployment-not-provisioned` when the deployment has never reached the provider (no `provider_app_id`); there is nothing for the provider to tear down. Trigger a deploy from the studio first.\n\nReturns the updated deployment row on success — same wire shape as GET, with `phase = stopped` and `public_url` cleared."}},"/api/v1/apps/{id}/archive":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"}],"responses":{"200":{"description":"The app row with `archived = true`.","content":{"application/json":{"schema":{"title":"App","description":"Public projection of an app the caller can see. Mutable fields are exposed via PATCH /api/v1/apps/{id}.","type":"object","properties":{"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"idea_id":{"description":"Linked idea id, when the app was scaffolded from an idea. May be null.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"archived":{"description":"True once the app has been archived. Archive/delete verbs are not yet exposed in v1.","type":"boolean"},"slug":{"description":"URL-safe identifier, unique within the org. Lowercase letters, digits, and hyphens; 1-60 chars.","type":"string"},"org_id":{"description":"Owning org id. Immutable: apps cannot be transferred between orgs via this api.","type":"string","format":"uuid"},"settings":{"description":"Free-form app-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"icon":{"description":"Single-glyph emoji shown alongside the title. Defaults to ✨.","oneOf":[{"type":"string"},{"type":"null"}]},"themes":{"description":"Theme catalog: {active, definitions}. Drives end-user theming in the canvas.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"title":{"description":"Human-readable name shown in UI. 1-200 chars.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"},"preview":{"description":"Optional preview image URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"locales":{"description":"The languages this app serves: `active` (the one it is authored in), `available` (the others it offers) and `glossary` (names a translator must not translate). Read what is translated, and run the translator, at GET /api/v1/apps/{app_id}/translations.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"}},"required":["id","org_id","slug","title","icon","archived","settings","themes","locales","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"Archive an app","description":"Soft-deletes the app: flips `archived` to true. The row and all its workspaces persist and stay queryable; clients typically hide archived apps. Reversible via /unarchive.\n\nAuthorization: `[:write :resource]` (owner/admin/member) on the app's org. 404 opacity for non-members."}},"/api/v1/deployments/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."}],"responses":{"200":{"description":"The deployment row.","content":{"application/json":{"schema":{"title":"Deployment","description":"Public projection of an app deployment. One row per (app, env) pair; phase + stage transition over the row's lifetime via the studio deploy flow.","type":"object","properties":{"slug":{"description":"Vanity label the app also serves on, as <slug>.orbitapps.ai. Unique across the platform; unlike a custom domain it needs no DNS record and no certificate. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"stage":{"description":"Sub-state when phase=building: bundle | apply | wait-ready. Nil otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"issues":{"description":"What the last deploy could not build. Empty on a clean deploy. The app still serves — phase stays \"deployed\" — but the named parts are missing, so their routes and schedules are not mounted.","type":"array","items":{"title":"DeploymentIssue","description":"One part of the app the last deploy could not build.","type":"object","properties":{"kind":{"description":"What kind of thing failed to build, e.g. \"workflow\".","oneOf":[{"type":"string"},{"type":"null"}]},"workspace_id":{"description":"Workspace that holds it, so a client can link straight to the editor.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Its name, as the author sees it in Studio.","oneOf":[{"type":"string"},{"type":"null"}]},"reason":{"description":"Grep-able rule name, e.g. \"workflow-preflight-failed\".","oneOf":[{"type":"string"},{"type":"null"}]},"message":{"description":"What went wrong, already written for the author.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["kind","workspace_id","title","reason","message"],"additionalProperties":false}},"started_at":{"description":"RFC 3339 timestamp the last deploy attempt began. May be null on a never-deployed row.","oneOf":[{"type":"string"},{"type":"null"}]},"completed_at":{"description":"RFC 3339 timestamp the last deploy attempt reached a terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"events":{"description":"Per-transition timeline (sliding window of last 50). Drives the UI timeline panel.","type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"ip_address":{"description":"Provider-assigned IP, when surfaced. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"app_id":{"description":"Owning app id. Immutable: deployments cannot be reassigned to another app.","type":"string","format":"uuid"},"history":{"description":"Per-attempt deploy history (sliding window of last 20). Drives the Versions list + revert.","type":"array","items":{"title":"DeploymentHistoryEntry","description":"One per-attempt summary; powers the Versions list + Revert.","type":"object","properties":{"at":{"description":"RFC 3339 timestamp the attempt reached terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_url":{"description":"Bundle pointer in R2 — passed back via deploy to revert to this version.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_version":{"description":"Short slug from the bundle path.","oneOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"description":"Profile id (uuid string) of whoever started the attempt. Nil for reconciler-driven transitions.","oneOf":[{"type":"string"},{"type":"null"}]},"outcome":{"description":"deployed | degraded | failed | stopped. \"degraded\" means the release went live but could not build every part — see the deployment's issues.","oneOf":[{"type":"string"},{"type":"null"}]},"error_code":{"description":"Machine-readable failure code on failed/stopped outcomes; nil on deployed.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["at","bundle_url","bundle_version","triggered_by","outcome","error_code"],"additionalProperties":false}},"phase":{"description":"High-level lifecycle phase. One of: idle | building | deployed | stopping | stopped | failed.","type":"string"},"public_url":{"description":"Publicly reachable URL once the deployment is live. May be null while building / failed.","oneOf":[{"type":"string"},{"type":"null"}]},"env":{"description":"Environment slot — one of \"dev\", \"uat\", or \"prod\". Each (app, env) pair has at most one deployment row.","type":"string"},"last_heartbeat_at":{"description":"RFC 3339 timestamp of the last successful watcher/reconciler tick. Drives staleness detection.","oneOf":[{"type":"string"},{"type":"null"}]},"provider_app_id":{"description":"Identifier returned by the hosting provider after the first successful deploy.","oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"description":"RFC 3339 timestamp of the most recent transition.","type":"string"},"status":{"description":"Uniform async-job status derived from phase: pending | running | succeeded | cancelled | failed. The same enum every long-running resource exposes — poll generically on this.","type":"string"},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"bundle_url":{"description":"R2 manifest URL of the bundle currently deployed. Used by revert.","oneOf":[{"type":"string"},{"type":"null"}]},"error":{"description":"Structured failure context (source / code / detail / at). Non-null only when phase=failed.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"bundle_version":{"description":"Short slug for the bundle currently deployed.","oneOf":[{"type":"string"},{"type":"null"}]},"domain":{"description":"Custom domain bound to this deployment, when configured. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"version":{"description":"Monotonic counter, bumped on every transition. Useful as a cache-invalidation key.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp. Stable across phase transitions.","type":"string"}},"required":["id","app_id","org_id","env","phase","status","issues","events","history","version","created_at","updated_at"],"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Get a deployment","description":"Returns the deployment's public metadata.\n\nVisibility: the caller must be an active member of the deployment's org. Non-members get 404 with the `deployment-not-found` spec — same Stripe-style opacity as the rest of the api."}},"/api/v1/executions/{id}/events":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Execution id from POST /api/v1/workspaces/{id}/executions"},{"in":"query","name":"after_seq","required":false,"schema":{"type":"integer"},"description":"Return only events with seq greater than this — same number SSE carries as the frame id."}],"responses":{"200":{"description":"JSON mode: events newer than after_seq, oldest first.","content":{"application/json":{"schema":{"title":"ExecutionEvents","type":"object","properties":{"data":{"type":"array","items":{"title":"ExecutionEvent","description":"One run event: node-start | node-complete | done | error.","type":"object","properties":{"seq":{"description":"Monotonic event address — resume cursor.","type":"integer"},"at":{"description":"RFC 3339 timestamp.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"node-start | node-complete | done | error.","oneOf":[{"type":"string"},{"type":"null"}]},"node_id":{"description":"The workflow node the event concerns."},"outputs":{"description":"Node outputs on node-complete."},"result":{"description":"Workflow result on done."},"message":{"description":"Failure message on error.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Execution does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Executions"],"summary":"Stream or fetch the execution's events","description":"The run's seq-addressed event log (node-start / node-complete / done / error), in two encodings:\n\n  • `Accept: application/json` (default) — `{data: [...]}`; pass `?after_seq=N` to fetch only newer events.\n  • `Accept: text/event-stream` — SSE: replays, then live-tails. Frame `id` = event `seq`; reconnects resume via `Last-Event-ID`. A final `event: done` frame closes the stream at terminal status.\n\nEvents are a sliding window of the last 200 entries.\n\nAuthorization: any active member (viewer allowed); 404 opacity for non-members."}},"/api/v1/blocks":{"get":{"parameters":[{"in":"query","name":"q","required":false,"schema":{"type":"string"},"description":"Natural-language BM25 query, e.g. `\"button\"`, `\"send webhook\"`. Omit for plain browse + filter."},{"in":"query","name":"kind","required":false,"schema":{"type":"string","enum":["component","node","integration","database","action","trigger"]},"description":"Block kind. `component` is a UI primitive; `node` / `integration` / `database` are workflow units; `action` / `trigger` are the operations an integration provides."},{"in":"query","name":"node","required":false,"schema":{"type":"string"},"description":"Restrict to the actions/triggers of one node, e.g. `github`."},{"in":"query","name":"category","required":false,"schema":{"type":"string"},"description":"Restrict to a palette category, e.g. `forms`, `layout`."},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Max rows returned. 1–100, default 20."},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0},"description":"Result offset for pagination. Default 0."}],"responses":{"200":{"description":"Matching blocks in the slim search shape.","content":{"application/json":{"schema":{"title":"BlockList","description":"List envelope. `limit` and `offset` echo the query; v1 omits a total count — detect end-of-results when `data.length < limit`.","type":"object","properties":{"data":{"type":"array","items":{"title":"BlockMatch","description":"Slim search hit. Read the full spec via GET /blocks/{slug}.","type":"object","properties":{"slug":{"description":"URL-safe handle, unique within its kind. The path param on GET /blocks/{slug}.","type":"string"},"kind":{"description":"Block kind.","type":"string"},"title":{"description":"Human-readable display name.","type":"string"},"description":{"description":"One-line summary for tooltips and ranking.","type":"string"},"node":{"description":"Owning node slug for `action` / `trigger` blocks, e.g. `github`.","type":"string"}},"required":["slug","kind","title","description"],"additionalProperties":false}},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["data","limit","offset"],"additionalProperties":false}}}}},"tags":["Blocks"],"summary":"Search and browse the block vocabulary","description":"Returns the blocks the caller can reach.\n\nSearch semantics:\n  • Pass `q` for BM25 full-text ranking across title, slug, description, and keywords.\n  • Omit `q` to plain-browse with `kind`/`node`/`category` filters.\n  • Combine `q` with filters to scope the search.\n\nUse `?node=<slug>&kind=action` to list one integration's actions.\n\nPagination is offset-based; the response omits a total count. Detect end-of-results when `data.length < limit`."}},"/api/v1/orgs/{id}/agents":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"}],"responses":{"200":{"content":{"application/json":{"schema":{"title":"AgentList","type":"object","properties":{"data":{"type":"array","items":{"title":"Agent","description":"One non-human (agent / integration) member.","type":"object","properties":{"role":{"type":"string"},"name":{"description":"Display name, as supplied on create.","oneOf":[{"type":"string"},{"type":"null"}]},"org_id":{"type":"string","format":"uuid"},"owner_member_id":{"description":"The human member that owns this agent.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"active | revoked.","type":"string"},"id":{"type":"string","format":"uuid"},"kind":{"description":"agent | integration.","type":"string"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","kind","owner_member_id","name","role","status"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Agents"],"summary":"List the org's agents","description":"Every agent / integration member in the org. Authorization: `[:read :member]`."},"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"}],"requestBody":{"content":{"application/json":{"schema":{"title":"AgentCreate","description":"Request body for POST /api/v1/orgs/{id}/agents.","type":"object","properties":{"name":{"description":"Agent + token display name.","allOf":[{"type":"string"},{}]},"role":{"description":"Membership role bounding the agent's max authority. `owner` is reserved.","type":"string","enum":["admin","member","viewer"]},"scopes":{"description":"Token scopes as [verb resource] pairs, e.g. [[\"write\" \"deployment\"]]. Defaults to the narrow-autonomous set (read + build apps). Grant billable/sensitive capabilities explicitly here.","oneOf":[{"type":"array","items":{"type":"array","items":{"type":"string"}}},{"type":"null"}]}},"required":["name"],"additionalProperties":false}}}},"responses":{"201":{"content":{"application/json":{"schema":{"title":"Agent","description":"One non-human (agent / integration) member.","type":"object","properties":{"role":{"type":"string"},"token_prefix":{"type":"string"},"name":{"description":"Display name, as supplied on create.","oneOf":[{"type":"string"},{"type":"null"}]},"org_id":{"type":"string","format":"uuid"},"token_id":{"type":"string","format":"uuid"},"owner_member_id":{"description":"The human member that owns this agent.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"token":{"description":"The token plaintext — shown only in this response. Store it now.","type":"string"},"status":{"description":"active | revoked.","type":"string"},"id":{"type":"string","format":"uuid"},"kind":{"description":"agent | integration.","type":"string"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","kind","owner_member_id","name","role","status","token","token_id","token_prefix"],"additionalProperties":false}}}},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Agents"],"summary":"Create an agent member and mint its token","description":"Creates an `agent`-kind member owned by the caller and mints an org-scoped token bound to it. The token plaintext is returned exactly once — store it immediately (e.g. as `ORBIT_TOKEN`).\n\nAuthorization: `[:manage :member]` + `[:manage :token]` (owner/admin)."}},"/api/v1/blocks/{slug}":{"get":{"parameters":[{"in":"path","name":"slug","required":true,"schema":{"type":"string"},"description":"The block's kebab-case slug, from a previous search hit."}],"responses":{"200":{"description":"The block.","content":{"application/json":{"schema":{"title":"Block","description":"A composable unit — UI component or workflow node/integration/action.","type":"object","properties":{"description":{"description":"Tagline plus 1–2 prose sentences.","type":"string"},"category":{"description":"Palette category, e.g. `forms`, `layout`.","type":"string"},"anatomy":{"description":"Compound-component grammar — sub-parts and their composition rules. {} for atomic blocks.","type":"object","additionalProperties":{}},"slug":{"description":"URL-safe handle, unique within its kind.","type":"string"},"node":{"description":"Owning node slug for `action` / `trigger` blocks.","type":"string"},"component":{"description":"React renderer dispatch key in PascalCase, e.g. `Button`. Absent for non-renderable blocks.","type":"string"},"title":{"description":"Human-readable display name.","type":"string"},"kind":{"description":"Block kind.","type":"string"},"body":{"description":"Kind-specific extras: component nesting/defaults; action outputs/annotations. {} when none.","type":"object","additionalProperties":{}},"props":{"description":"The block's prop surface — a vector of setter descriptors (or a JSON-Schema map for some kinds). [] / {} when it has none.","anyOf":[{"type":"array","items":{}},{"type":"object","additionalProperties":{}}]}},"required":["slug","kind","title","description","props","anatomy","body"],"additionalProperties":false}}}},"404":{"description":"No block matches the slug.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Blocks"],"summary":"Get one block","description":"Reads the full block for the slug in the path — `props` (JSON Schema), `anatomy` (compound-part grammar), and `body` (nesting/defaults for components, outputs/annotations for actions)."}},"/api/v1/apps/{id}/scratchpad":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id."}],"responses":{"200":{"description":"The board.","content":{"application/json":{"schema":{"title":"Scratchpad","description":"A board plus its flat card and link lists.","type":"object","properties":{"slug":{"oneOf":[{"type":"string"},{"type":"null"}]},"org_id":{"type":"string","format":"uuid"},"app_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"settings":{"type":"object","additionalProperties":{}},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"cards":{"type":"array","items":{"title":"Card","description":"One card on the board.","type":"object","properties":{"y":{"type":"number"},"content_url":{"description":"Where a file card's bytes are served, when they are public. Null otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"content":{"description":"Kind-specific payload. An `image` or `file` card carries `artifact_id`, `filename` and `mime_type`.","type":"object","additionalProperties":{}},"width":{"oneOf":[{"type":"number"},{"type":"null"}]},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"z":{"type":"integer"},"status":{"description":"`draft` | `ready` | `failed` | `accepted`.","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Renderer family: `spark` | `text` | `image` | `file` | `website` | `preview` | `idea` | `hill` | `artifact`.","type":"string"},"x":{"type":"number"},"artifact_kind":{"description":"Structured outcome an artifact card holds: `insights`. Unrelated to a stored file.","oneOf":[{"type":"string"},{"type":"null"}]},"scratchpad_id":{"description":"Owning board id.","type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"height":{"oneOf":[{"type":"number"},{"type":"null"}]},"props":{"description":"Presentation options that are not the material itself.","type":"object","additionalProperties":{}}},"required":["id","scratchpad_id","kind","content","props","x","y","z","created_at"],"additionalProperties":false}},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"links":{"type":"array","items":{"title":"Link","description":"A wire between two cards on one board.","type":"object","properties":{"from_id":{"type":"string","format":"uuid"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"kind":{"description":"`input` compiles to a dependency; `reference` is provenance and may form cycles.","type":"string"},"to_port":{"type":"string"},"from_port":{"type":"string"},"idx":{"type":"integer"},"to_id":{"type":"string","format":"uuid"},"scratchpad_id":{"type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","scratchpad_id","from_id","to_id","kind","from_port","to_port","idx","created_at"],"additionalProperties":false}}},"required":["id","org_id","app_id","settings","cards","links","created_at"],"additionalProperties":false}}}},"404":{"description":"No app, or no board yet.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Scratchpad"],"summary":"Get an app's scratchpad","description":"The board, with every card and link on it. A card that carries a file names it in `content.artifact_id`, and `content_url` is where those bytes are served when they are public."},"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id."}],"responses":{"200":{"description":"The board that already existed.","content":{"application/json":{"schema":{"title":"Scratchpad","description":"A board plus its flat card and link lists.","type":"object","properties":{"slug":{"oneOf":[{"type":"string"},{"type":"null"}]},"org_id":{"type":"string","format":"uuid"},"app_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"settings":{"type":"object","additionalProperties":{}},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"cards":{"type":"array","items":{"title":"Card","description":"One card on the board.","type":"object","properties":{"y":{"type":"number"},"content_url":{"description":"Where a file card's bytes are served, when they are public. Null otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"content":{"description":"Kind-specific payload. An `image` or `file` card carries `artifact_id`, `filename` and `mime_type`.","type":"object","additionalProperties":{}},"width":{"oneOf":[{"type":"number"},{"type":"null"}]},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"z":{"type":"integer"},"status":{"description":"`draft` | `ready` | `failed` | `accepted`.","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Renderer family: `spark` | `text` | `image` | `file` | `website` | `preview` | `idea` | `hill` | `artifact`.","type":"string"},"x":{"type":"number"},"artifact_kind":{"description":"Structured outcome an artifact card holds: `insights`. Unrelated to a stored file.","oneOf":[{"type":"string"},{"type":"null"}]},"scratchpad_id":{"description":"Owning board id.","type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"height":{"oneOf":[{"type":"number"},{"type":"null"}]},"props":{"description":"Presentation options that are not the material itself.","type":"object","additionalProperties":{}}},"required":["id","scratchpad_id","kind","content","props","x","y","z","created_at"],"additionalProperties":false}},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"links":{"type":"array","items":{"title":"Link","description":"A wire between two cards on one board.","type":"object","properties":{"from_id":{"type":"string","format":"uuid"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"kind":{"description":"`input` compiles to a dependency; `reference` is provenance and may form cycles.","type":"string"},"to_port":{"type":"string"},"from_port":{"type":"string"},"idx":{"type":"integer"},"to_id":{"type":"string","format":"uuid"},"scratchpad_id":{"type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","scratchpad_id","from_id","to_id","kind","from_port","to_port","idx","created_at"],"additionalProperties":false}}},"required":["id","org_id","app_id","settings","cards","links","created_at"],"additionalProperties":false}}}},"201":{"description":"The board just created.","content":{"application/json":{"schema":{"title":"Scratchpad","description":"A board plus its flat card and link lists.","type":"object","properties":{"slug":{"oneOf":[{"type":"string"},{"type":"null"}]},"org_id":{"type":"string","format":"uuid"},"app_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"settings":{"type":"object","additionalProperties":{}},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"cards":{"type":"array","items":{"title":"Card","description":"One card on the board.","type":"object","properties":{"y":{"type":"number"},"content_url":{"description":"Where a file card's bytes are served, when they are public. Null otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"content":{"description":"Kind-specific payload. An `image` or `file` card carries `artifact_id`, `filename` and `mime_type`.","type":"object","additionalProperties":{}},"width":{"oneOf":[{"type":"number"},{"type":"null"}]},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"z":{"type":"integer"},"status":{"description":"`draft` | `ready` | `failed` | `accepted`.","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Renderer family: `spark` | `text` | `image` | `file` | `website` | `preview` | `idea` | `hill` | `artifact`.","type":"string"},"x":{"type":"number"},"artifact_kind":{"description":"Structured outcome an artifact card holds: `insights`. Unrelated to a stored file.","oneOf":[{"type":"string"},{"type":"null"}]},"scratchpad_id":{"description":"Owning board id.","type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"height":{"oneOf":[{"type":"number"},{"type":"null"}]},"props":{"description":"Presentation options that are not the material itself.","type":"object","additionalProperties":{}}},"required":["id","scratchpad_id","kind","content","props","x","y","z","created_at"],"additionalProperties":false}},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"links":{"type":"array","items":{"title":"Link","description":"A wire between two cards on one board.","type":"object","properties":{"from_id":{"type":"string","format":"uuid"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"kind":{"description":"`input` compiles to a dependency; `reference` is provenance and may form cycles.","type":"string"},"to_port":{"type":"string"},"from_port":{"type":"string"},"idx":{"type":"integer"},"to_id":{"type":"string","format":"uuid"},"scratchpad_id":{"type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","scratchpad_id","from_id","to_id","kind","from_port","to_port","idx","created_at"],"additionalProperties":false}}},"required":["id","org_id","app_id","settings","cards","links","created_at"],"additionalProperties":false}}}},"404":{"description":"No app with that id.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Scratchpad"],"summary":"Create an app's scratchpad","description":"Find-or-create. An app made through this api already has a board and this answers 200 with it; an older app gets one and this answers 201."}},"/api/v1/apps/{app_id}/translations":{"get":{"parameters":[{"in":"path","name":"app_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"},{"in":"query","name":"locale","required":false,"schema":{"type":"string"},"description":"Restrict to one language. Omit for a per-language summary."},{"in":"query","name":"pending","required":false,"schema":{"type":"boolean"},"description":"Return the strings this language still needs instead of the ones it has — the authored English awaiting a translation, each with the `source_key` a translation is submitted under. Ignores `status`."},{"in":"query","name":"status","required":false,"schema":{"type":"string","enum":["machine","reviewed","stale"]},"description":"Only stored strings with this review status. Ignored with `pending`."},{"in":"query","name":"run_id","required":false,"schema":{"type":"string","format":"uuid"},"description":"Report this exact run rather than the language's newest."},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":200},"description":"Strings per page, 1-200 (default 50). Detect the end when strings.length < limit."},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0},"description":"Rows to skip (default 0)."}],"responses":{"200":{"description":"The app's languages, and either a summary of all or the detail of one.","content":{"application/json":{"schema":{"title":"TranslationsRead","description":"One shape whether or not the request names a language. With `locale`: `plan`, `run` and `strings` describe that one. Without it: `summary` carries a row per available language and `strings` is empty.","type":"object","properties":{"locales":{"title":"AppLocales","description":"The languages the app serves. Written through PATCH /api/v1/apps/{id}.","type":"object","properties":{"active":{"description":"The language the app is authored in. Never translated into itself.","type":"string"},"available":{"description":"The other languages it offers.","type":"array","items":{"type":"string"}},"glossary":{"description":"Names a model must not translate, such as the product's own.","type":"array","items":{"type":"string"}}},"required":["active","available","glossary"],"additionalProperties":false},"locale":{"description":"The language this reply describes, or null when it describes all of them.","oneOf":[{"type":"string"},{"type":"null"}]},"plan":{"oneOf":[{"title":"TranslationPlan","description":"What a run would do for one language, costing nothing to ask.","type":"object","properties":{"total":{"description":"Distinct authored strings in the app.","type":"integer"},"pending":{"description":"How many of them this language still needs. 0 means nothing to run.","type":"integer"},"orphans":{"description":"Stored strings the app no longer contains. Reported, never deleted.","type":"integer"}},"required":["total","pending","orphans"],"additionalProperties":false},{"type":"null"}]},"run":{"oneOf":[{"title":"TranslationRun","description":"One run of one language, in the uniform async-job shape.","type":"object","properties":{"id":{"description":"Run id, the same value POST answered with.","type":"string"},"locale":{"type":"string"},"status":{"description":"queued → running → succeeded | failed | cancelled.","type":"string","enum":["queued","running","succeeded","failed","cancelled"]},"attempt":{"description":"Which attempt is current; a failed run is retried up to max_attempts.","type":"integer"},"max_attempts":{"type":"integer"},"error":{"description":"The last failure this run recorded, when it has one.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","locale","status","attempt","max_attempts","error"],"additionalProperties":false},{"type":"null"}]},"summary":{"type":"array","items":{"title":"TranslationSummary","description":"One language's state. Present for every available language when the request names none.","type":"object","properties":{"locale":{"type":"string"},"total":{"type":"integer"},"pending":{"type":"integer"},"orphans":{"type":"integer"},"run_status":{"description":"How this language's newest run ended, or null when it has never had one.","oneOf":[{"type":"string","enum":["queued","running","succeeded","failed","cancelled"]},{"type":"null"}]}},"required":["locale","total","pending","orphans","run_status"],"additionalProperties":false}},"strings":{"type":"array","items":{"title":"Translation","description":"One string of this language. A translated one carries its `content`; one the request asked for with `pending` has none yet, and its stored-only fields are null — there is no row until something translates it.","type":"object","properties":{"format":{"description":"`plain` for body copy, `attribute` for a placeholder, alt or aria-* value.","type":"string"},"locale":{"type":"string"},"content":{"description":"The translation, or null when there is none yet.","oneOf":[{"type":"string"},{"type":"null"}]},"source":{"description":"The authored English.","type":"string"},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"source_key":{"description":"Hash of the authored string, and the key a translation is submitted under. Stable across restyling and re-use, so a re-run never re-bills it.","type":"string"},"status":{"description":"`reviewed` is a person's own wording, and no run overwrites it. Null when untranslated.","oneOf":[{"type":"string","enum":["machine","reviewed","stale"]},{"type":"null"}]},"id":{"description":"Null for a string nothing has translated yet.","oneOf":[{"type":"string"},{"type":"null"}]},"context":{"description":"The author's disambiguator, when they wrote one.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","locale","source_key","source","content","status","format","context","updated_at"],"additionalProperties":false}},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["locales","locale","plan","run","summary","strings","limit","offset"],"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Translations"],"summary":"Read an app's translations","description":"Everything about one app's languages in one request: which it offers, how much of each is translated, how its newest run is going, and the stored strings.\n\nOmit `locale` for a `summary` row per available language, each with its `run_status` — the call to make first. Pass `locale` for that one language's `plan`, its `run`, and the stored strings themselves.\n\nAdd `pending=true` and `strings` carries what the language still NEEDS instead: the authored English awaiting translation, each with the `source_key` to submit it under. That is the read for a caller who intends to translate the app itself — POST them back as `strings`.\n\n`plan.pending` is what a run would still have to do, so a client that started one re-reads this until `pending` reaches 0 or `run.status` turns terminal. `run` is the newest run of that language unless `run_id` names another.\n\nPagination is offset-based over `strings`; the response carries no total count. Detect the end when `strings.length < limit`.\n\nVisibility mirrors GET /api/v1/apps/{id}: 404 opacity for non-members."},"post":{"parameters":[{"in":"path","name":"app_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"}],"requestBody":{"content":{"application/json":{"schema":{"title":"TranslationRunStart","description":"Which language to translate, and whether to redo what a machine already did.","type":"object","properties":{"locale":{"description":"A language from the app's `available` list. The authored language is refused.","type":"string"},"force":{"description":"Re-translate rows a previous run wrote. Reviewed rows are never redone. Default false. Ignored with `strings`.","type":"boolean"},"strings":{"description":"Translations you already have, as `{source_key: text}` — the keys come from this endpoint's `pending` read. Sending them STORES them instead of starting a run, so no model is asked and the org's provider is not needed at all. A key the app's text no longer hashes to is named back rather than stored, and a string a person reviewed is left alone.","type":"object","additionalProperties":{"type":"string","minLength":1}},"model":{"description":"Who produced those translations, recorded on each row as its only trace of provenance — a model id such as `claude-opus-5`. Defaults to `caller-supplied`. Only read with `strings`.","type":"string","maxLength":100}},"required":["locale"],"additionalProperties":false}}}},"responses":{"202":{"description":"The queued run.","content":{"application/json":{"schema":{"title":"TranslationRunStarted","description":"What the request did, and what the language still needs afterwards.","type":"object","properties":{"run_id":{"description":"The run to watch, or null when nothing was started — nothing was outstanding, or you supplied the strings yourself.","oneOf":[{"type":"string"},{"type":"null"}]},"stored":{"description":"How many supplied strings were saved. 0 when a run was started instead.","type":"integer"},"unknown":{"description":"Supplied keys the app's text no longer hashes to. Named rather than stored, because a row under one is a translation nothing would ever look up.","type":"array","items":{"type":"string"}},"locale":{"type":"string"},"status":{"type":"string","enum":["queued","running","succeeded","failed","cancelled"]},"total":{"type":"integer"},"pending":{"type":"integer"}},"required":["run_id","stored","unknown","locale","status","total","pending"],"additionalProperties":false}}}},"200":{"description":"Supplied strings were stored, or nothing was outstanding — either way no run was started and nothing was spent.","content":{"application/json":{"schema":{"title":"TranslationRunStarted","description":"What the request did, and what the language still needs afterwards.","type":"object","properties":{"run_id":{"description":"The run to watch, or null when nothing was started — nothing was outstanding, or you supplied the strings yourself.","oneOf":[{"type":"string"},{"type":"null"}]},"stored":{"description":"How many supplied strings were saved. 0 when a run was started instead.","type":"integer"},"unknown":{"description":"Supplied keys the app's text no longer hashes to. Named rather than stored, because a row under one is a translation nothing would ever look up.","type":"array","items":{"type":"string"}},"locale":{"type":"string"},"status":{"type":"string","enum":["queued","running","succeeded","failed","cancelled"]},"total":{"type":"integer"},"pending":{"type":"integer"}},"required":["run_id","stored","unknown","locale","status","total","pending"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"The language is not offered, is the authored one, or the org has no AI provider connected.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Translations"],"summary":"Translate an app into one language","description":"Two ways to fill a language, and the body decides which.\n\nWITH `strings` — `{source_key: text}`, the keys from this endpoint's `pending` read — the translations are STORED and no model is asked. No AI provider is needed, nothing is spent, and a caller that is itself a translator can fill an app this way. A key the app's text no longer hashes to comes back in `unknown` rather than being stored; a string a person reviewed is left alone. Rows are written `machine` and carry `model`, so a later forced run may still redo them.\n\nWITHOUT `strings` — starts a run and answers 202 immediately with its `run_id`. A run takes minutes: progress arrives through GET /api/v1/apps/{app_id}/translations?locale=<locale>, where `run.status` moves queued → running → succeeded | failed and `plan.pending` falls as strings land.\n\nThe language must already be on the app's `available` list; add it with PATCH /api/v1/apps/{id} first. The app's own authored language is refused — its pages already serve that text.\n\nA run is idempotent by construction: it translates what is outstanding and skips what is stored, so re-running after a failure pays only for the remainder, and a run with nothing outstanding answers 200 without starting anything. A string a person corrected is never overwritten, `force` included.\n\nA run asks the org's own provider, on its own plan. With none connected a RUN is refused with 422 rather than enqueued — supplying `strings` needs no provider and is never refused for that.\n\nAuthorization: `[:write :resource]` (owner/admin/member); viewer is rejected with 403. Supports the `Idempotency-Key` header."}},"/api/v1/deployments/{id}/abort":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."}],"responses":{"200":{"description":"The deployment row with the cancellation recorded; still building.","content":{"application/json":{"schema":{"title":"Deployment","description":"Public projection of an app deployment. One row per (app, env) pair; phase + stage transition over the row's lifetime via the studio deploy flow.","type":"object","properties":{"slug":{"description":"Vanity label the app also serves on, as <slug>.orbitapps.ai. Unique across the platform; unlike a custom domain it needs no DNS record and no certificate. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"stage":{"description":"Sub-state when phase=building: bundle | apply | wait-ready. Nil otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"issues":{"description":"What the last deploy could not build. Empty on a clean deploy. The app still serves — phase stays \"deployed\" — but the named parts are missing, so their routes and schedules are not mounted.","type":"array","items":{"title":"DeploymentIssue","description":"One part of the app the last deploy could not build.","type":"object","properties":{"kind":{"description":"What kind of thing failed to build, e.g. \"workflow\".","oneOf":[{"type":"string"},{"type":"null"}]},"workspace_id":{"description":"Workspace that holds it, so a client can link straight to the editor.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Its name, as the author sees it in Studio.","oneOf":[{"type":"string"},{"type":"null"}]},"reason":{"description":"Grep-able rule name, e.g. \"workflow-preflight-failed\".","oneOf":[{"type":"string"},{"type":"null"}]},"message":{"description":"What went wrong, already written for the author.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["kind","workspace_id","title","reason","message"],"additionalProperties":false}},"started_at":{"description":"RFC 3339 timestamp the last deploy attempt began. May be null on a never-deployed row.","oneOf":[{"type":"string"},{"type":"null"}]},"completed_at":{"description":"RFC 3339 timestamp the last deploy attempt reached a terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"events":{"description":"Per-transition timeline (sliding window of last 50). Drives the UI timeline panel.","type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"ip_address":{"description":"Provider-assigned IP, when surfaced. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"app_id":{"description":"Owning app id. Immutable: deployments cannot be reassigned to another app.","type":"string","format":"uuid"},"history":{"description":"Per-attempt deploy history (sliding window of last 20). Drives the Versions list + revert.","type":"array","items":{"title":"DeploymentHistoryEntry","description":"One per-attempt summary; powers the Versions list + Revert.","type":"object","properties":{"at":{"description":"RFC 3339 timestamp the attempt reached terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_url":{"description":"Bundle pointer in R2 — passed back via deploy to revert to this version.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_version":{"description":"Short slug from the bundle path.","oneOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"description":"Profile id (uuid string) of whoever started the attempt. Nil for reconciler-driven transitions.","oneOf":[{"type":"string"},{"type":"null"}]},"outcome":{"description":"deployed | degraded | failed | stopped. \"degraded\" means the release went live but could not build every part — see the deployment's issues.","oneOf":[{"type":"string"},{"type":"null"}]},"error_code":{"description":"Machine-readable failure code on failed/stopped outcomes; nil on deployed.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["at","bundle_url","bundle_version","triggered_by","outcome","error_code"],"additionalProperties":false}},"phase":{"description":"High-level lifecycle phase. One of: idle | building | deployed | stopping | stopped | failed.","type":"string"},"public_url":{"description":"Publicly reachable URL once the deployment is live. May be null while building / failed.","oneOf":[{"type":"string"},{"type":"null"}]},"env":{"description":"Environment slot — one of \"dev\", \"uat\", or \"prod\". Each (app, env) pair has at most one deployment row.","type":"string"},"last_heartbeat_at":{"description":"RFC 3339 timestamp of the last successful watcher/reconciler tick. Drives staleness detection.","oneOf":[{"type":"string"},{"type":"null"}]},"provider_app_id":{"description":"Identifier returned by the hosting provider after the first successful deploy.","oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"description":"RFC 3339 timestamp of the most recent transition.","type":"string"},"status":{"description":"Uniform async-job status derived from phase: pending | running | succeeded | cancelled | failed. The same enum every long-running resource exposes — poll generically on this.","type":"string"},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"bundle_url":{"description":"R2 manifest URL of the bundle currently deployed. Used by revert.","oneOf":[{"type":"string"},{"type":"null"}]},"error":{"description":"Structured failure context (source / code / detail / at). Non-null only when phase=failed.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"bundle_version":{"description":"Short slug for the bundle currently deployed.","oneOf":[{"type":"string"},{"type":"null"}]},"domain":{"description":"Custom domain bound to this deployment, when configured. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"version":{"description":"Monotonic counter, bumped on every transition. Useful as a cache-invalidation key.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp. Stable across phase transitions.","type":"string"}},"required":["id","app_id","org_id","env","phase","status","issues","events","history","version","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility on the deployment but lacks the role to mutate it.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Abort an in-flight deploy","description":"Requests cancellation of the background deploy (bundle upload, provider apply, readiness watcher). The request body is empty.\n\nThe response is the still-`building` row. The deploy worker acts on the request and lands the row terminal: back to `deployed` when a previous attempt left an app live — that app keeps serving and is never torn down — or `stopped` when this attempt was the one that put resources up. Poll `GET /deployments/{id}` or the events stream for the terminal row.\n\nAuthorization: `[:write :resource]` (owner/admin/member). Viewer is rejected with 403."}},"/api/v1/scratchpads/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Scratchpad id."}],"responses":{"200":{"description":"The board.","content":{"application/json":{"schema":{"title":"Scratchpad","description":"A board plus its flat card and link lists.","type":"object","properties":{"slug":{"oneOf":[{"type":"string"},{"type":"null"}]},"org_id":{"type":"string","format":"uuid"},"app_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"settings":{"type":"object","additionalProperties":{}},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"cards":{"type":"array","items":{"title":"Card","description":"One card on the board.","type":"object","properties":{"y":{"type":"number"},"content_url":{"description":"Where a file card's bytes are served, when they are public. Null otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"content":{"description":"Kind-specific payload. An `image` or `file` card carries `artifact_id`, `filename` and `mime_type`.","type":"object","additionalProperties":{}},"width":{"oneOf":[{"type":"number"},{"type":"null"}]},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"z":{"type":"integer"},"status":{"description":"`draft` | `ready` | `failed` | `accepted`.","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Renderer family: `spark` | `text` | `image` | `file` | `website` | `preview` | `idea` | `hill` | `artifact`.","type":"string"},"x":{"type":"number"},"artifact_kind":{"description":"Structured outcome an artifact card holds: `insights`. Unrelated to a stored file.","oneOf":[{"type":"string"},{"type":"null"}]},"scratchpad_id":{"description":"Owning board id.","type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"height":{"oneOf":[{"type":"number"},{"type":"null"}]},"props":{"description":"Presentation options that are not the material itself.","type":"object","additionalProperties":{}}},"required":["id","scratchpad_id","kind","content","props","x","y","z","created_at"],"additionalProperties":false}},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"links":{"type":"array","items":{"title":"Link","description":"A wire between two cards on one board.","type":"object","properties":{"from_id":{"type":"string","format":"uuid"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"kind":{"description":"`input` compiles to a dependency; `reference` is provenance and may form cycles.","type":"string"},"to_port":{"type":"string"},"from_port":{"type":"string"},"idx":{"type":"integer"},"to_id":{"type":"string","format":"uuid"},"scratchpad_id":{"type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","scratchpad_id","from_id","to_id","kind","from_port","to_port","idx","created_at"],"additionalProperties":false}}},"required":["id","org_id","app_id","settings","cards","links","created_at"],"additionalProperties":false}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Scratchpad"],"summary":"Get a scratchpad"},"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Scratchpad id."}],"requestBody":{"content":{"application/json":{"schema":{"title":"ScratchpadPatch","description":"Partial update of the board itself. Its cards and links are edited by id.","type":"object","properties":{"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"settings":{"description":"Replaced wholesale; merge before sending.","type":"object","additionalProperties":{}},"archived":{"description":"True removes the row from the board, false puts it back. A removal archives rather than deletes, so the row stays readable and a restore can undo it.","type":"boolean"}},"additionalProperties":false}}}},"responses":{"200":{"description":"The board after the change, without its cards and wires.","content":{"application/json":{"schema":{"title":"ScratchpadSummary","description":"One board as it appears in a list: what it is and what it belongs to. Its cards and links are not here — read one board with `GET /scratchpads/{id}` for those.","type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string","format":"uuid"},"app_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"slug":{"oneOf":[{"type":"string"},{"type":"null"}]},"settings":{"type":"object","additionalProperties":{}},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","app_id","settings","created_at","updated_at"],"additionalProperties":false}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Somebody else changed it first.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Scratchpad"],"summary":"Rename or remove a scratchpad","description":"Rename the board or change its settings. `archived: true` takes it off the shelf and `false` puts it back — the board and everything on it stay readable either way."}},"/api/v1/deployments/{id}/domains":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."}],"responses":{"200":{"description":"Attached custom domains.","content":{"application/json":{"schema":{"title":"CustomDomainList","type":"object","properties":{"data":{"type":"array","items":{"title":"CustomDomain","description":"One custom domain attached to the deployment, with the DNS record the owner must create.","type":"object","properties":{"hostname":{"description":"The customer-owned hostname (e.g. shop.acme.com).","oneOf":[{"type":"string"},{"type":"null"}]},"kind":{"description":"cname (subdomain) | apex (root domain, A record).","oneOf":[{"type":"string"},{"type":"null"}]},"dns_status":{"description":"pending | verified | failed.","oneOf":[{"type":"string"},{"type":"null"}]},"cert_status":{"description":"none | issuing | ready | failed.","oneOf":[{"type":"string"},{"type":"null"}]},"cert_reason":{"description":"Why the cert failed (e.g. secret-quota-exhausted, issuing-timeout, apply-rejected).","oneOf":[{"type":"string"},{"type":"null"}]},"dns_type":{"description":"The record type to create: CNAME or A.","oneOf":[{"type":"string"},{"type":"null"}]},"dns_target":{"description":"What the record must point at — the app host (CNAME) or the load balancer IP (A).","oneOf":[{"type":"string"},{"type":"null"}]},"checked_at":{"description":"Last DNS check.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["hostname","kind","dns_status","cert_status","dns_type","dns_target"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"List the deployment's custom domains","description":"Every attached hostname with its verification state and the DNS record the domain owner must create (`dns_type` + `dns_target`).\n\nAuthorization: any active member (viewer allowed)."},"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."}],"requestBody":{"content":{"application/json":{"schema":{"title":"CustomDomainAdd","description":"Request body for POST /api/v1/deployments/{id}/domains.","type":"object","properties":{"hostname":{"description":"The hostname to attach. Lowercase RFC-1123; no scheme/path.","type":"string"},"kind":{"description":"cname | apex. Omit to derive from the hostname.","type":"string","enum":["cname","apex"]}},"required":["hostname"],"additionalProperties":false}}}},"responses":{"201":{"description":"The pending domain entry, carrying the DNS record to create.","content":{"application/json":{"schema":{"title":"CustomDomain","description":"One custom domain attached to the deployment, with the DNS record the owner must create.","type":"object","properties":{"hostname":{"description":"The customer-owned hostname (e.g. shop.acme.com).","oneOf":[{"type":"string"},{"type":"null"}]},"kind":{"description":"cname (subdomain) | apex (root domain, A record).","oneOf":[{"type":"string"},{"type":"null"}]},"dns_status":{"description":"pending | verified | failed.","oneOf":[{"type":"string"},{"type":"null"}]},"cert_status":{"description":"none | issuing | ready | failed.","oneOf":[{"type":"string"},{"type":"null"}]},"cert_reason":{"description":"Why the cert failed (e.g. secret-quota-exhausted, issuing-timeout, apply-rejected).","oneOf":[{"type":"string"},{"type":"null"}]},"dns_type":{"description":"The record type to create: CNAME or A.","oneOf":[{"type":"string"},{"type":"null"}]},"dns_target":{"description":"What the record must point at — the app host (CNAME) or the load balancer IP (A).","oneOf":[{"type":"string"},{"type":"null"}]},"checked_at":{"description":"Last DNS check.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["hostname","kind","dns_status","cert_status","dns_type","dns_target"],"additionalProperties":false}}}},"403":{"description":"Caller lacks the role to manage domains.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"The hostname is not a valid lowercase RFC-1123 name.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Attach a custom domain","description":"Stamps a pending entry and returns it with the DNS record to create: a CNAME pointing at `dns_target` for subdomains, or an A record at the load balancer IP for apex domains. Create the record, then call POST .../domains/{hostname}/verify.\n\nNo cluster mutation happens yet — DNS isn't verified.\n\nAuthorization: `[:manage :resource]` (owner/admin) — domains route live traffic."}},"/api/v1/deployments/{id}/refresh":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."}],"responses":{"200":{"description":"The deployment row after the provider poll.","content":{"application/json":{"schema":{"title":"Deployment","description":"Public projection of an app deployment. One row per (app, env) pair; phase + stage transition over the row's lifetime via the studio deploy flow.","type":"object","properties":{"slug":{"description":"Vanity label the app also serves on, as <slug>.orbitapps.ai. Unique across the platform; unlike a custom domain it needs no DNS record and no certificate. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"stage":{"description":"Sub-state when phase=building: bundle | apply | wait-ready. Nil otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"issues":{"description":"What the last deploy could not build. Empty on a clean deploy. The app still serves — phase stays \"deployed\" — but the named parts are missing, so their routes and schedules are not mounted.","type":"array","items":{"title":"DeploymentIssue","description":"One part of the app the last deploy could not build.","type":"object","properties":{"kind":{"description":"What kind of thing failed to build, e.g. \"workflow\".","oneOf":[{"type":"string"},{"type":"null"}]},"workspace_id":{"description":"Workspace that holds it, so a client can link straight to the editor.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Its name, as the author sees it in Studio.","oneOf":[{"type":"string"},{"type":"null"}]},"reason":{"description":"Grep-able rule name, e.g. \"workflow-preflight-failed\".","oneOf":[{"type":"string"},{"type":"null"}]},"message":{"description":"What went wrong, already written for the author.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["kind","workspace_id","title","reason","message"],"additionalProperties":false}},"started_at":{"description":"RFC 3339 timestamp the last deploy attempt began. May be null on a never-deployed row.","oneOf":[{"type":"string"},{"type":"null"}]},"completed_at":{"description":"RFC 3339 timestamp the last deploy attempt reached a terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"events":{"description":"Per-transition timeline (sliding window of last 50). Drives the UI timeline panel.","type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"ip_address":{"description":"Provider-assigned IP, when surfaced. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"app_id":{"description":"Owning app id. Immutable: deployments cannot be reassigned to another app.","type":"string","format":"uuid"},"history":{"description":"Per-attempt deploy history (sliding window of last 20). Drives the Versions list + revert.","type":"array","items":{"title":"DeploymentHistoryEntry","description":"One per-attempt summary; powers the Versions list + Revert.","type":"object","properties":{"at":{"description":"RFC 3339 timestamp the attempt reached terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_url":{"description":"Bundle pointer in R2 — passed back via deploy to revert to this version.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_version":{"description":"Short slug from the bundle path.","oneOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"description":"Profile id (uuid string) of whoever started the attempt. Nil for reconciler-driven transitions.","oneOf":[{"type":"string"},{"type":"null"}]},"outcome":{"description":"deployed | degraded | failed | stopped. \"degraded\" means the release went live but could not build every part — see the deployment's issues.","oneOf":[{"type":"string"},{"type":"null"}]},"error_code":{"description":"Machine-readable failure code on failed/stopped outcomes; nil on deployed.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["at","bundle_url","bundle_version","triggered_by","outcome","error_code"],"additionalProperties":false}},"phase":{"description":"High-level lifecycle phase. One of: idle | building | deployed | stopping | stopped | failed.","type":"string"},"public_url":{"description":"Publicly reachable URL once the deployment is live. May be null while building / failed.","oneOf":[{"type":"string"},{"type":"null"}]},"env":{"description":"Environment slot — one of \"dev\", \"uat\", or \"prod\". Each (app, env) pair has at most one deployment row.","type":"string"},"last_heartbeat_at":{"description":"RFC 3339 timestamp of the last successful watcher/reconciler tick. Drives staleness detection.","oneOf":[{"type":"string"},{"type":"null"}]},"provider_app_id":{"description":"Identifier returned by the hosting provider after the first successful deploy.","oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"description":"RFC 3339 timestamp of the most recent transition.","type":"string"},"status":{"description":"Uniform async-job status derived from phase: pending | running | succeeded | cancelled | failed. The same enum every long-running resource exposes — poll generically on this.","type":"string"},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"bundle_url":{"description":"R2 manifest URL of the bundle currently deployed. Used by revert.","oneOf":[{"type":"string"},{"type":"null"}]},"error":{"description":"Structured failure context (source / code / detail / at). Non-null only when phase=failed.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"bundle_version":{"description":"Short slug for the bundle currently deployed.","oneOf":[{"type":"string"},{"type":"null"}]},"domain":{"description":"Custom domain bound to this deployment, when configured. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"version":{"description":"Monotonic counter, bumped on every transition. Useful as a cache-invalidation key.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp. Stable across phase transitions.","type":"string"}},"required":["id","app_id","org_id","env","phase","status","issues","events","history","version","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility on the deployment but lacks the role to mutate it.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Deployment has no provider_app_id — never successfully provisioned.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Refresh a deployment's status from the provider","description":"Polls the configured hosting provider for the deployment's current status and public URL, writes the result back to the row, and returns the updated wire shape. The request body is empty.\n\nUseful when a deploy is in-flight or the local view is stale: GET returns whatever atlas last wrote, while this verb forces a live re-read from the provider.\n\nAuthorization: refresh writes through to atlas (bumps version, appends an event, and may transition the row to a terminal phase based on the provider's response) so it requires `[:write :resource]` (owner/admin/member). Viewer is rejected with 403.\n\nReturns 409 `deployment-not-provisioned` when the deployment has no `provider_app_id`; there is no provider object to poll."}},"/api/v1/images/{slug}":{"get":{"parameters":[{"in":"path","name":"slug","required":true,"schema":{"type":"string"},"description":"The image's slug, from a previous search hit."}],"responses":{"200":{"description":"The image.","content":{"application/json":{"schema":{"title":"ImageMatch","description":"A search hit, directly placeable: hotlink `src`, render `blurhash` while loading, ALWAYS display `attribution`.","type":"object","properties":{"role":{"description":"Placement hint: hero | avatar | thumbnail | background | gallery | logo.","oneOf":[{"type":"string"},{"type":"null"}]},"description":{"description":"What the photo shows.","oneOf":[{"type":"string"},{"type":"null"}]},"thumb":{"description":"Smaller variant for grids.","oneOf":[{"type":"string"},{"type":"null"}]},"category":{"description":"Topic bucket, e.g. healthcare, saas, realestate, people. One value per topic set — a sub-specialty like dental or legal is searchable via `q`, not here.","oneOf":[{"type":"string"},{"type":"null"}]},"copy_space":{"description":"Where the photo stays flat enough for headline text to sit on it: left | right | top | bottom | center, or none when the frame is busy edge to edge. Put the copy on that side.","oneOf":[{"type":"string"},{"type":"null"}]},"aspect":{"description":"Aspect ratio, e.g. 16:9.","oneOf":[{"type":"string"},{"type":"null"}]},"slug":{"description":"URL-safe handle. The path param on GET /images/{slug}.","type":"string"},"color":{"description":"Dominant hex color — use as the loading background.","oneOf":[{"type":"string"},{"type":"null"}]},"license":{"description":"License identifier, e.g. unsplash.","oneOf":[{"type":"string"},{"type":"null"}]},"alt":{"description":"Accessibility text.","oneOf":[{"type":"string"},{"type":"null"}]},"width":{"description":"Source pixels.","oneOf":[{"type":"integer"},{"type":"null"}]},"orientation":{"description":"landscape | portrait | square.","oneOf":[{"type":"string"},{"type":"null"}]},"src":{"description":"Hotlink URL (provider CDN). Never mirror or re-host.","oneOf":[{"type":"string"},{"type":"null"}]},"source":{"description":"Provider: unsplash | pexels | pixabay | custom.","oneOf":[{"type":"string"},{"type":"null"}]},"keywords":{"description":"Space-separated search keywords.","oneOf":[{"type":"string"},{"type":"null"}]},"blurhash":{"description":"BlurHash placeholder string for CLS-free loading.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Human-readable title.","oneOf":[{"type":"string"},{"type":"null"}]},"attribution":{"description":"Photographer credit — {photographer, username, profile_url, source_url}. Display wherever the image renders.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"height":{"description":"Source pixels.","oneOf":[{"type":"integer"},{"type":"null"}]}},"required":["slug","title","description","category","role","orientation","aspect","width","height","color","blurhash","copy_space","src","alt","attribution"],"additionalProperties":false}}}},"404":{"description":"No image matches the slug.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Images"],"summary":"Get one image","description":"The full catalog entry, adding keywords / license / source to the search-hit shape."}},"/api/v1/orgs/{id}/secrets":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"}],"responses":{"200":{"description":"Secret metadata for the org.","content":{"application/json":{"schema":{"title":"SecretList","type":"object","properties":{"data":{"type":"array","items":{"title":"Secret","description":"Secret metadata. Values are write-only — no endpoint returns them.","type":"object","properties":{"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"accessed_at":{"description":"Last time a workflow/deploy decrypted this secret.","oneOf":[{"type":"string"},{"type":"null"}]},"name":{"description":"Unique name within the org — the address workflows reference.","type":"string"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"key_version":{"description":"Encryption key version of the stored ciphertext.","oneOf":[{"type":"integer"},{"type":"null"}]},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"valid | invalid (provider validation outcome).","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"provider":{"description":"Provider tag (e.g. github, stripe) when the secret is a known integration credential.","oneOf":[{"type":"string"},{"type":"null"}]},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","name","status"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"403":{"description":"Caller's role cannot read secrets.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Org does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Secrets"],"summary":"List the org's secrets (metadata only)","description":"Names, providers, statuses, and timestamps — never values or ciphertext. Internal per-app deploy keys are excluded.\n\nAuthorization: `[:read :secret]` (owner/admin/member; viewer is rejected with 403)."}},"/api/v1/artifacts":{"post":{"responses":{"201":{"description":"The created artifact.","content":{"application/json":{"schema":{"type":"object","properties":{"original_name":{"type":"string"},"content_url":{"oneOf":[{"type":"string"},{"type":"null"}]},"mime_type":{"type":"string"},"app_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"width":{"oneOf":[{"type":"integer"},{"type":"null"}]},"checksum":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"url":{"oneOf":[{"type":"string"},{"type":"null"}]},"visibility":{"type":"string"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"height":{"oneOf":[{"type":"integer"},{"type":"null"}]},"size_bytes":{"type":"integer"}},"required":["id","app_id","original_name","mime_type","size_bytes","visibility","url","content_url","created_at"],"additionalProperties":false}}}},"403":{"description":"Token has no org scope.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Artifacts"],"summary":"Upload an artifact","description":"Store a file (multipart field `file`) and record its metadata. Optional fields: `app` scopes it to one app, `visibility` (`private` default, or `public` to serve the bytes without a token)."}},"/api/v1/scratchpads/{id}/links":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Scratchpad id."}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"from_id":{"description":"Card the wire leaves.","type":"string","format":"uuid"},"to_id":{"description":"Card the wire enters.","type":"string","format":"uuid"},"kind":{"description":"`reference` (default) or `input`.","oneOf":[{"type":"string"},{"type":"null"}]},"label":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["from_id","to_id"],"additionalProperties":false}}}},"responses":{"201":{"description":"The link just made.","content":{"application/json":{"schema":{"title":"Link","description":"A wire between two cards on one board.","type":"object","properties":{"from_id":{"type":"string","format":"uuid"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"kind":{"description":"`input` compiles to a dependency; `reference` is provenance and may form cycles.","type":"string"},"to_port":{"type":"string"},"from_port":{"type":"string"},"idx":{"type":"integer"},"to_id":{"type":"string","format":"uuid"},"scratchpad_id":{"type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","scratchpad_id","from_id","to_id","kind","from_port","to_port","idx","created_at"],"additionalProperties":false}}}},"404":{"description":"No such board.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"An endpoint is not on this board.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Scratchpad"],"summary":"Connect two cards","description":"Wire one card to another. `reference` records that one informed the other and may form cycles; `input` compiles to a dependency a run reads."}},"/api/v1/recipes/{slug}":{"get":{"parameters":[{"in":"path","name":"slug","required":true,"schema":{"type":"string"},"description":"The recipe's kebab-case slug, from a previous search hit."},{"in":"query","name":"view","required":false,"schema":{"default":"full","type":"string","enum":["summary","outline","full"]},"description":"Response detail: summary | outline | full. Default full."}],"responses":{"200":{"description":"The recipe.","content":{"application/json":{"schema":{"title":"Recipe","description":"A composition of blocks — a section or page to reference and rebuild from.","type":"object","properties":{"slug":{"description":"URL-safe handle, unique within its kind.","type":"string"},"kind":{"description":"Recipe kind.","type":"string"},"title":{"description":"Human-readable display name.","type":"string"},"description":{"description":"Tagline plus 1–2 prose sentences.","type":"string"},"blocks":{"description":"Block slugs this recipe composes. Fetch each via GET /blocks/{slug}.","type":"array","items":{"type":"string"}},"composition":{"description":"The arrangement to reference — `{mode, tree}` for `view=full`, `{mode, ops}` for `view=outline`. Absent for `view=summary`, which does not read it. A pattern to build something similar from, not a verbatim stamp.","type":"object","additionalProperties":{}},"family":{"description":"Variant family, e.g. `header`.","type":"string"},"category":{"description":"Broader bucket, e.g. `marketing`.","type":"string"}},"required":["slug","kind","title","description","blocks"],"additionalProperties":false}}}},"404":{"description":"No recipe matches the slug.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Recipes"],"summary":"Get one recipe","description":"Reads the full recipe for the slug in the path — the `blocks` it composes (fetch each via GET /blocks/{slug}) and `composition`, the arrangement to reference and build something similar from (not a verbatim stamp)."}},"/api/v1/artifacts/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Artifact id."}],"responses":{"200":{"description":"Artifact metadata + fresh url.","content":{"application/json":{"schema":{"type":"object","properties":{"original_name":{"type":"string"},"content_url":{"oneOf":[{"type":"string"},{"type":"null"}]},"mime_type":{"type":"string"},"app_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"width":{"oneOf":[{"type":"integer"},{"type":"null"}]},"checksum":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"url":{"oneOf":[{"type":"string"},{"type":"null"}]},"visibility":{"type":"string"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"height":{"oneOf":[{"type":"integer"},{"type":"null"}]},"size_bytes":{"type":"integer"}},"required":["id","app_id","original_name","mime_type","size_bytes","visibility","url","content_url","created_at"],"additionalProperties":false}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Artifacts"],"summary":"Get an artifact"},"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Artifact id."}],"responses":{"204":{"description":"Artifact deleted."},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Artifacts"],"summary":"Delete an artifact"}},"/api/v1/deployments/{id}/restart":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."}],"responses":{"200":{"description":"The deployment row after the restart, with `version` bumped and a `user-restart` event appended.","content":{"application/json":{"schema":{"title":"Deployment","description":"Public projection of an app deployment. One row per (app, env) pair; phase + stage transition over the row's lifetime via the studio deploy flow.","type":"object","properties":{"slug":{"description":"Vanity label the app also serves on, as <slug>.orbitapps.ai. Unique across the platform; unlike a custom domain it needs no DNS record and no certificate. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"stage":{"description":"Sub-state when phase=building: bundle | apply | wait-ready. Nil otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"issues":{"description":"What the last deploy could not build. Empty on a clean deploy. The app still serves — phase stays \"deployed\" — but the named parts are missing, so their routes and schedules are not mounted.","type":"array","items":{"title":"DeploymentIssue","description":"One part of the app the last deploy could not build.","type":"object","properties":{"kind":{"description":"What kind of thing failed to build, e.g. \"workflow\".","oneOf":[{"type":"string"},{"type":"null"}]},"workspace_id":{"description":"Workspace that holds it, so a client can link straight to the editor.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Its name, as the author sees it in Studio.","oneOf":[{"type":"string"},{"type":"null"}]},"reason":{"description":"Grep-able rule name, e.g. \"workflow-preflight-failed\".","oneOf":[{"type":"string"},{"type":"null"}]},"message":{"description":"What went wrong, already written for the author.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["kind","workspace_id","title","reason","message"],"additionalProperties":false}},"started_at":{"description":"RFC 3339 timestamp the last deploy attempt began. May be null on a never-deployed row.","oneOf":[{"type":"string"},{"type":"null"}]},"completed_at":{"description":"RFC 3339 timestamp the last deploy attempt reached a terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"events":{"description":"Per-transition timeline (sliding window of last 50). Drives the UI timeline panel.","type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"ip_address":{"description":"Provider-assigned IP, when surfaced. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"app_id":{"description":"Owning app id. Immutable: deployments cannot be reassigned to another app.","type":"string","format":"uuid"},"history":{"description":"Per-attempt deploy history (sliding window of last 20). Drives the Versions list + revert.","type":"array","items":{"title":"DeploymentHistoryEntry","description":"One per-attempt summary; powers the Versions list + Revert.","type":"object","properties":{"at":{"description":"RFC 3339 timestamp the attempt reached terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_url":{"description":"Bundle pointer in R2 — passed back via deploy to revert to this version.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_version":{"description":"Short slug from the bundle path.","oneOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"description":"Profile id (uuid string) of whoever started the attempt. Nil for reconciler-driven transitions.","oneOf":[{"type":"string"},{"type":"null"}]},"outcome":{"description":"deployed | degraded | failed | stopped. \"degraded\" means the release went live but could not build every part — see the deployment's issues.","oneOf":[{"type":"string"},{"type":"null"}]},"error_code":{"description":"Machine-readable failure code on failed/stopped outcomes; nil on deployed.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["at","bundle_url","bundle_version","triggered_by","outcome","error_code"],"additionalProperties":false}},"phase":{"description":"High-level lifecycle phase. One of: idle | building | deployed | stopping | stopped | failed.","type":"string"},"public_url":{"description":"Publicly reachable URL once the deployment is live. May be null while building / failed.","oneOf":[{"type":"string"},{"type":"null"}]},"env":{"description":"Environment slot — one of \"dev\", \"uat\", or \"prod\". Each (app, env) pair has at most one deployment row.","type":"string"},"last_heartbeat_at":{"description":"RFC 3339 timestamp of the last successful watcher/reconciler tick. Drives staleness detection.","oneOf":[{"type":"string"},{"type":"null"}]},"provider_app_id":{"description":"Identifier returned by the hosting provider after the first successful deploy.","oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"description":"RFC 3339 timestamp of the most recent transition.","type":"string"},"status":{"description":"Uniform async-job status derived from phase: pending | running | succeeded | cancelled | failed. The same enum every long-running resource exposes — poll generically on this.","type":"string"},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"bundle_url":{"description":"R2 manifest URL of the bundle currently deployed. Used by revert.","oneOf":[{"type":"string"},{"type":"null"}]},"error":{"description":"Structured failure context (source / code / detail / at). Non-null only when phase=failed.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"bundle_version":{"description":"Short slug for the bundle currently deployed.","oneOf":[{"type":"string"},{"type":"null"}]},"domain":{"description":"Custom domain bound to this deployment, when configured. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"version":{"description":"Monotonic counter, bumped on every transition. Useful as a cache-invalidation key.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp. Stable across phase transitions.","type":"string"}},"required":["id","app_id","org_id","env","phase","status","issues","events","history","version","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility on the deployment but lacks the role to mutate it.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Deployment phase is not `deployed`; restart is rejected.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Restart a deployment","description":"Triggers a rolling restart of the active deployment via the configured hosting provider. The request body is empty — the deployment to restart is fully identified by the path.\n\nAuthorization: the caller must be an active member of the deployment's org with a role granting `[:write :resource]` (owner, admin, or member). Viewer is rejected with 403 carrying the `insufficient-role` spec.\n\nPhase guard: restart is only valid when the row's `phase` is `\"deployed\"`. Any other phase returns 409 `deployment-not-restartable` carrying the current phase in `detail`. Use POST /refresh to sync from the provider first if you suspect the local row is stale.\n\nOn success: the row's `phase` stays `deployed`, `version` bumps, and a `user-restart` event is appended. The provider performs the rolling restart asynchronously — poll POST /refresh to confirm convergence."}},"/api/v1/orgs/{id}/ledger":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":50},"description":"Max rows (1-50, default 20)."},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0},"description":"Result offset (default 0)."}],"responses":{"200":{"description":"Ledger entries, newest first.","content":{"application/json":{"schema":{"title":"Ledger","type":"object","properties":{"data":{"type":"array","items":{"title":"LedgerEntry","description":"One immutable credit movement.","type":"object","properties":{"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"amount":{"description":"Cents moved.","oneOf":[{"type":"integer"},{"type":"null"}]},"reversal_of":{"description":"The entry this one reverses, for refunds.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"unit_cost":{"oneOf":[{"type":"integer"},{"type":"null"}]},"entry_type":{"description":"credit | debit.","oneOf":[{"type":"string"},{"type":"null"}]},"balance_after":{"description":"Wallet balance after this entry.","oneOf":[{"type":"integer"},{"type":"null"}]},"reference":{"description":"External correlator — invoice id, deployment id.","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"tx_type":{"description":"purchase | bonus | usage | refund | adjustment.","oneOf":[{"type":"string"},{"type":"null"}]},"units":{"oneOf":[{"type":"integer"},{"type":"null"}]},"feature":{"description":"Consuming feature on debits (hosting, ai).","oneOf":[{"type":"string"},{"type":"null"}]},"provider":{"oneOf":[{"type":"string"},{"type":"null"}]},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","entry_type","tx_type","amount","balance_after"],"additionalProperties":false}},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["data","limit","offset"],"additionalProperties":false}}}},"404":{"description":"Org does not exist, or the caller has no visibility (intentionally identical shapes).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Billing"],"summary":"Get the org's credit ledger","description":"The immutable audit trail of credit movements — purchases, bonuses, usage settlements, refunds — newest first, offset-paginated (limit 1-50, default 20). The end of the trail is reached when fewer than `limit` entries return.\n\nAuthorization: any active member (viewer included)."}},"/api/v1/tokens/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object","properties":{"scopes":{"oneOf":[{"type":"array","items":{}},{"type":"null"}]},"revoked_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"expires_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"name":{"type":"string"},"org_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"last_used_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"scope":{"type":"string"},"member_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"prefix":{"type":"string"},"updated_at":{"type":"string"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"profile_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"created_at":{"type":"string"}},"required":["id","name","prefix","kind","scope","scopes","created_at","updated_at"],"additionalProperties":false}}}},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Tokens"],"summary":"Get token metadata","description":"Returns the token's metadata. Never returns the plaintext — that's only visible at create time."},"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Tokens"],"summary":"Revoke a token","description":"Soft-delete: sets revoked-at. The token immediately stops authenticating but the row stays for audit."},"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"allOf":[{"type":"string"},{}]}},"additionalProperties":false}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object","properties":{"scopes":{"oneOf":[{"type":"array","items":{}},{"type":"null"}]},"revoked_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"expires_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"name":{"type":"string"},"org_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"last_used_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"scope":{"type":"string"},"member_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"prefix":{"type":"string"},"updated_at":{"type":"string"},"id":{"type":"string","format":"uuid"},"kind":{"type":"string"},"profile_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"created_at":{"type":"string"}},"required":["id","name","prefix","kind","scope","scopes","created_at","updated_at"],"additionalProperties":false}}}},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Tokens"],"summary":"Update a token (name only for now)"}},"/api/v1/orgs/{id}/secrets/{name}":{"put":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"},{"in":"path","name":"name","required":true,"schema":{"type":"string"},"description":"Secret name — unique within the org; may contain `/` (matched as a catch-all). Workflows reference secrets by the row id from the list endpoint."}],"requestBody":{"content":{"application/json":{"schema":{"title":"SecretPut","description":"Create or replace the named secret's value. The value is encrypted at rest and never readable back through this api.","type":"object","properties":{"value":{"description":"The plaintext credential to encrypt and store.","allOf":[{"type":"string"},{}]},"provider":{"description":"Provider tag, e.g. github.","oneOf":[{"type":"string"},{"type":"null"}]},"description":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["value"],"additionalProperties":false}}}},"responses":{"200":{"description":"The re-encrypted secret's metadata.","content":{"application/json":{"schema":{"title":"Secret","description":"Secret metadata. Values are write-only — no endpoint returns them.","type":"object","properties":{"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"accessed_at":{"description":"Last time a workflow/deploy decrypted this secret.","oneOf":[{"type":"string"},{"type":"null"}]},"name":{"description":"Unique name within the org — the address workflows reference.","type":"string"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"key_version":{"description":"Encryption key version of the stored ciphertext.","oneOf":[{"type":"integer"},{"type":"null"}]},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"valid | invalid (provider validation outcome).","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"provider":{"description":"Provider tag (e.g. github, stripe) when the secret is a known integration credential.","oneOf":[{"type":"string"},{"type":"null"}]},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","name","status"],"additionalProperties":false}}}},"201":{"description":"The created secret's metadata.","content":{"application/json":{"schema":{"title":"Secret","description":"Secret metadata. Values are write-only — no endpoint returns them.","type":"object","properties":{"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"accessed_at":{"description":"Last time a workflow/deploy decrypted this secret.","oneOf":[{"type":"string"},{"type":"null"}]},"name":{"description":"Unique name within the org — the address workflows reference.","type":"string"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"key_version":{"description":"Encryption key version of the stored ciphertext.","oneOf":[{"type":"integer"},{"type":"null"}]},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"valid | invalid (provider validation outcome).","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"provider":{"description":"Provider tag (e.g. github, stripe) when the secret is a known integration credential.","oneOf":[{"type":"string"},{"type":"null"}]},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","name","status"],"additionalProperties":false}}}},"403":{"description":"Caller's role cannot write secrets.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Org does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"503":{"description":"The api instance has no vault master key configured.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Secrets"],"summary":"Create or replace a secret","description":"Encrypts `value` (AES-256-GCM envelope) and stores it under `name`. PUT on an existing name re-encrypts in place (same id, so workflow references keep working) — 200; a new name creates — 201.\n\nValues are write-only: neither this response nor any other endpoint returns the plaintext. Workflows and deploys decrypt internally at run time.\n\nAuthorization: `[:manage :secret]` (owner/admin)."},"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"},{"in":"path","name":"name","required":true,"schema":{"type":"string"},"description":"Secret name — unique within the org; may contain `/` (matched as a catch-all). Workflows reference secrets by the row id from the list endpoint."}],"responses":{"204":{"description":"Secret deleted."},"403":{"description":"Caller's role cannot write secrets.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Org or secret not found / not visible.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Secrets"],"summary":"Delete a secret","description":"Permanently removes the named secret. Workflows that reference it will fail their next run — update them first.\n\nAuthorization: `[:manage :secret]` (owner/admin)."}},"/api/v1/apps/{id}/theme":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"}],"responses":{"200":{"description":"The app's theme vocabulary.","content":{"application/json":{"schema":{"title":"AppTheme","description":"The app's active theme vocabulary — target these when authoring class props.","type":"object","properties":{"tokens":{"description":"Active theme token map: `{color {name value}, font {name value}}`. `{}` when the app has no active theme (the base semantic tokens still apply).","type":"object","additionalProperties":{}},"classes":{"description":"Ready-to-use on-theme utility classes derived from the semantic color tokens (`bg-/text-/border-<name>`).","type":"array","items":{"type":"string"}},"palettes":{"description":"Tailwind's built-in palette families — they render but don't follow the app's light/dark theme, so prefer the semantic tokens above.","type":"array","items":{"type":"string"}},"note":{"description":"Authoring guidance.","type":"string"}},"required":["tokens","classes","palettes","note"],"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"Get the app's theme vocabulary","description":"Returns the app's active theme tokens plus a derived list of on-theme utility classes (`bg-/text-/border-<name>`) to author with. Prefer these semantic tokens over raw Tailwind palettes, which render but don't follow the app's light/dark theme.\n\nVisibility mirrors GET /api/v1/apps/{id}: 404 opacity for non-members."}},"/api/v1/device/code":{"post":{"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"oneOf":[{"title":"OAuthDeviceCodeRequest","type":"object","properties":{"client_id":{"type":"string","minLength":1,"maxLength":64},"scope":{"type":"string"}},"additionalProperties":false},{"type":"null"}]}}}},"responses":{"200":{"description":"The grant. Show user_code, then poll /device/token every `interval` seconds.","content":{"application/json":{"schema":{"title":"DeviceCodeResponse","description":"RFC 8628 device authorization response.","type":"object","properties":{"device_code":{"description":"Secret the device polls /device/token with. Never show it to the user.","type":"string"},"user_code":{"description":"Human-typable code the user enters in Studio, shaped XXXX-XXXX.","type":"string"},"verification_uri":{"description":"Where the user approves the grant.","type":"string"},"verification_uri_complete":{"description":"Same, with the user_code pre-filled — render as a link or QR.","type":"string"},"expires_in":{"description":"Grant lifetime in seconds (15 minutes).","type":"integer"},"interval":{"description":"Minimum seconds between /device/token polls.","type":"integer"}},"required":["device_code","user_code","verification_uri","verification_uri_complete","expires_in","interval"],"additionalProperties":false}}}}},"tags":["Device"],"summary":"Start a device-authorization grant","description":"Begins the RFC 8628 device flow: returns a secret `device_code` (poll with it) and a human-typable `user_code` (show it, or render `verification_uri_complete` as a link/QR). The user approves at the verification URI in Studio, choosing the token's scope (personal, or one of their orgs).\n\nAnonymous — this is how a CLI gets its first token. Grants expire after 15 minutes.","security":[]}},"/api/v1/deployments/{id}/events":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Deployment id (UUIDv7). Obtain from GET /api/v1/apps/{app_id}/deployments."},{"in":"query","name":"after_seq","required":false,"schema":{"type":"integer"},"description":"Return only events with seq greater than this. Resume cursor — same number SSE carries as the frame id."}],"responses":{"200":{"description":"JSON mode: events newer than after_seq, oldest first. SSE mode streams the same entries as `transition` frames.","content":{"application/json":{"schema":{"title":"DeploymentEvents","description":"Seq-addressed transition log (sliding window of the last 50 transitions).","type":"object","properties":{"data":{"type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Deployment does not exist, or the caller is not an active member of the deployment's org. Response shape is identical so the caller can't distinguish the two.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Stream or fetch the deployment's transition events","description":"The deployment's seq-addressed transition log, in two encodings of the same data:\n\n  • `Accept: application/json` (default) — returns `{data: [...]}`. Pass `?after_seq=N` to fetch only newer events; the end of the in-flight deploy is reached when the row's `status` turns terminal.\n  • `Accept: text/event-stream` — Server-Sent Events: replays history, then live-tails. Each frame's `id` is the event's `seq`, so a dropped connection resumes loss-free — browsers send `Last-Event-ID` automatically; other clients may pass `?after_seq=` instead. A final `event: done` frame closes the stream once the deployment reaches a terminal status.\n\nEvents are a sliding window of the last 50 transitions; `seq` equals the row's `version` at transition time and stays stable as the window slides (events recorded before seq stamping carry seq=0).\n\nAuthorization: any active member of the deployment's org (read-only — viewer allowed). 404 opacity for non-members."}},"/api/v1/orgs":{"get":{"responses":{"200":{"description":"Visible organizations.","content":{"application/json":{"schema":{"title":"OrganizationList","description":"Paginated-ready envelope. v1 returns the entire visible set in `data`; future revisions may add pagination cursors alongside.","type":"object","properties":{"data":{"type":"array","items":{"title":"Organization","description":"Public projection of an organization the caller can see. Mutable fields are exposed via PATCH /api/v1/orgs/{id}.","type":"object","properties":{"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"display_name":{"description":"Human-readable org name shown in UI. Required, 1-200 chars.","type":"string"},"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"image":{"description":"Optional logo URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"settings":{"description":"Free-form org-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"archived":{"description":"True once the org has been archived. Archived orgs remain visible to existing members but can't be PATCHed via this api.","type":"boolean"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"}},"required":["id","display_name","settings","archived","created_at","updated_at"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}}},"tags":["Orgs"],"summary":"List the caller's organizations","description":"Returns every organization the calling token can read.\n\nScope semantics:\n  • profile-scoped tokens → all orgs where the caller has an active member row.\n  • org-scoped tokens     → a single-element array containing the token's own org.\n\nUse this as your first call after authenticating: it tells you which org_ids are addressable for subsequent /api/v1/orgs/{id} and /api/v1/apps requests."}},"/api/v1/scratchpads/{id}/cards":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Scratchpad id."}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"y":{"type":"number"},"content":{"description":"Kind-specific payload, for a card that carries no file.","type":"object","additionalProperties":{}},"file":{"description":"The bytes to store. Exactly one of `url`, `data_base64` or `artifact_id`.","type":"object","properties":{"url":{"description":"Public http(s) address for Orbit to fetch.","oneOf":[{"type":"string"},{"type":"null"}]},"data_base64":{"description":"The bytes inline, base64. A `data:` url is accepted as written.","oneOf":[{"type":"string"},{"type":"null"}]},"artifact_id":{"description":"A file already stored.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"filename":{"description":"Name to record, e.g. `logo.svg`.","oneOf":[{"type":"string"},{"type":"null"}]},"alt":{"description":"What the file shows, for a reader who cannot see it.","oneOf":[{"type":"string"},{"type":"null"}]}},"additionalProperties":false},"public":{"description":"Serve the file's bytes without a token — what a page rendering an `<img>` needs. Default false.","type":"boolean"},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"link":{"description":"A web address to keep as a link card — a page, a video, a competitor's site. Unlike `file.url`, the address is the material and nothing is downloaded. A bare host is accepted; `https` is filled in.","oneOf":[{"type":"string"},{"type":"null"}]},"kind":{"description":"Required when no `file` or `link` is given; inferred otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"x":{"description":"Board position. Omitted, the server places the card beside what is already there.","type":"number"},"props":{"type":"object","additionalProperties":{}}},"additionalProperties":false}}}},"responses":{"201":{"description":"The card just placed.","content":{"application/json":{"schema":{"title":"Card","description":"One card on the board.","type":"object","properties":{"y":{"type":"number"},"content_url":{"description":"Where a file card's bytes are served, when they are public. Null otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"content":{"description":"Kind-specific payload. An `image` or `file` card carries `artifact_id`, `filename` and `mime_type`.","type":"object","additionalProperties":{}},"width":{"oneOf":[{"type":"number"},{"type":"null"}]},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"z":{"type":"integer"},"status":{"description":"`draft` | `ready` | `failed` | `accepted`.","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Renderer family: `spark` | `text` | `image` | `file` | `website` | `preview` | `idea` | `hill` | `artifact`.","type":"string"},"x":{"type":"number"},"artifact_kind":{"description":"Structured outcome an artifact card holds: `insights`. Unrelated to a stored file.","oneOf":[{"type":"string"},{"type":"null"}]},"scratchpad_id":{"description":"Owning board id.","type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"height":{"oneOf":[{"type":"number"},{"type":"null"}]},"props":{"description":"Presentation options that are not the material itself.","type":"object","additionalProperties":{}}},"required":["id","scratchpad_id","kind","content","props","x","y","z","created_at"],"additionalProperties":false}}}},"404":{"description":"No such board.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"The kind or the file was refused.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Scratchpad"],"summary":"Place a card","description":"Put material on the board. This is how a file gets into Orbit: pass `file` with a `url`, `data_base64`, or an `artifact_id` you already hold, and the bytes are stored, recorded, and made into a card — an `image` card for a picture, a `file` card for anything else.\n\nPass `public: true` for a file a page renders: the card's `content_url` is then a stable address that answers without a token, which is what an `<img>` needs.\n\nWithout `file`, pass `kind` and `content` for an ordinary card — a `spark` or a `text` note left on the board.\n\nOmit `x`/`y` and the server places the card beside what is already there."}},"/api/v1/workspaces/{id}/executions":{"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Workflow workspace id from GET /api/v1/apps/{app_id}/workspaces?kind=workflow"}],"requestBody":{"content":{"application/json":{"schema":{"title":"ExecutionTrigger","description":"Request body for POST /api/v1/workspaces/{id}/executions.","type":"object","properties":{"inputs":{"description":"Initial inputs handed to the workflow's start node. The run also receives a `session` map ($.session.org-id / $.session.profile-id) automatically.","type":"object","additionalProperties":{}}},"additionalProperties":false}}}},"responses":{"202":{"description":"The running execution. Poll the Location header until a terminal status.","content":{"application/json":{"schema":{"title":"Execution","description":"One workflow run — the uniform async-job shape. Poll until `status` turns terminal, or tail /events.","type":"object","properties":{"workspace_id":{"description":"The workflow workspace this run executed.","type":"string","format":"uuid"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"app_id":{"description":"Owning app id.","type":"string","format":"uuid"},"updated_at":{"description":"RFC 3339 timestamp.","oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"running | succeeded | failed | cancelled.","type":"string"},"result":{"description":"The workflow's result once status=succeeded. Null while running."},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"error":{"description":"Structured failure ({node_id, message}) once status=failed.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"seq":{"description":"Latest event's seq — resume /events with ?after_seq= from here.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","app_id","workspace_id","status","seq","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to run workflows.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Workspace does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"The workspace is not a workflow, or the workflow has no executable nodes.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Executions"],"summary":"Run a workflow","description":"Executes the workspace's workflow against the app's pre-deploy sandbox and answers 202 immediately with the running execution. Poll GET /api/v1/executions/{id} (the `Location` header) until `status` turns terminal, or tail GET /api/v1/executions/{id}/events.\n\n`inputs` seeds the start node; the run also receives a `session` map ($.session.org-id / $.session.profile-id). Data nodes write to a per-app sandbox database — the same semantics as Studio's Run button, isolated from platform data and from the deployed app.\n\nAuthorization: `[:write :resource]` (owner/admin/member); viewer is rejected with 403. 404 opacity for non-members.\n\nSupports the `Idempotency-Key` header — a retried trigger replays the original 202 instead of starting a second run."}},"/api/v1/orgs/{id}/members":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"},{"in":"query","name":"kind","required":false,"schema":{"type":"string","enum":["human","agent","integration","runner","all"]},"description":"Which principals to list. Defaults to `human`, so a caller that asks for nothing keeps getting people. `all` returns every row in the org."}],"responses":{"200":{"description":"Membership rows for the org.","content":{"application/json":{"schema":{"title":"MemberList","type":"object","properties":{"data":{"type":"array","items":{"title":"Member","description":"One org membership row.","type":"object","properties":{"role":{"description":"owner | admin | member | viewer.","type":"string"},"email":{"description":"The membership's email. Null for a non-human member — an agent has no inbox.","oneOf":[{"type":"string"},{"type":"null"}]},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"invited | active | suspended.","type":"string"},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"human | agent | integration | runner. Rows written before kinds existed read as `human`.","type":"string"},"joined_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"profile_id":{"description":"Linked profile once the invite is accepted. Null while invited.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"invited_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","profile_id","kind","email","role","status"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"Org does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Members"],"summary":"List the org's members","description":"Every membership row — active, invited, suspended. Any active member may read (viewer included). 404 opacity for non-members.\n\nPeople by default. Pass `?kind=agent`, `integration` or `runner` for one class of non-human principal, or `?kind=all` to read the org as a single list. Agents are also managed on their own resource, `/api/v1/orgs/{id}/agents`, which mints their tokens."},"post":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"}],"requestBody":{"content":{"application/json":{"schema":{"title":"MemberInvite","description":"Request body for POST /api/v1/orgs/{id}/members.","type":"object","properties":{"email":{"description":"Invitee's email. One member row per email per org.","allOf":[{"type":"string"},{}]},"role":{"description":"Assignable roles. `owner` is reserved — ownership transfer is a separate studio flow.","type":"string","enum":["admin","member","viewer"]}},"required":["email","role"],"additionalProperties":false}}}},"responses":{"201":{"description":"The invited member row, including the one-time invite token.","content":{"application/json":{"schema":{"title":"Member","description":"One org membership row.","type":"object","properties":{"role":{"description":"owner | admin | member | viewer.","type":"string"},"email":{"description":"The membership's email. Null for a non-human member — an agent has no inbox.","oneOf":[{"type":"string"},{"type":"null"}]},"invite_token":{"description":"One-time invite token — shown only in this response. The invitee accepts at <studio>/org/{org_id}/members?invite_token={token}.","type":"string"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"invited | active | suspended.","type":"string"},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"human | agent | integration | runner. Rows written before kinds existed read as `human`.","type":"string"},"joined_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"profile_id":{"description":"Linked profile once the invite is accepted. Null while invited.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"invited_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","profile_id","kind","email","role","status","invite_token"],"additionalProperties":false}}}},"403":{"description":"Caller lacks the role to invite.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Org does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"A member row with this email already exists.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Members"],"summary":"Invite a teammate","description":"Creates an `invited` member row and returns it with the one-time `invite_token` — shown only in this response.\n\nThe api does NOT send the invite email; deliver the link yourself: `<studio>/org/{org_id}/members?invite_token={token}`.\n\n`owner` is not assignable. An email that already has a member row returns 409 `member-exists`.\n\nAuthorization: `[:write :member]` (owner/admin)."}},"/api/v1/scratchpads":{"get":{"parameters":[{"in":"query","name":"org_id","required":false,"schema":{"type":"string","format":"uuid"},"description":"Org to scope to."}],"responses":{"200":{"description":"The boards, newest activity first.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"title":"ScratchpadSummary","description":"One board as it appears in a list: what it is and what it belongs to. Its cards and links are not here — read one board with `GET /scratchpads/{id}` for those.","type":"object","properties":{"id":{"type":"string","format":"uuid"},"org_id":{"type":"string","format":"uuid"},"app_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"slug":{"oneOf":[{"type":"string"},{"type":"null"}]},"settings":{"type":"object","additionalProperties":{}},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","app_id","settings","created_at","updated_at"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}}},"tags":["Scratchpad"],"summary":"List scratchpads","description":"The org's boards, most recently touched first. Each row says what the board is and what it belongs to; its cards and links are not included — read one board with `GET /scratchpads/{id}` for those.\n\nScope with `?org_id`, or send the `X-Orbit-Org` header. With neither, this lists across every org you belong to. An org you are not a member of yields an empty list."}},"/api/v1/links/{id}":{"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Link id."}],"requestBody":{"content":{"application/json":{"schema":{"title":"LinkPatch","description":"Partial update of a wire. Its endpoints are not editable: a wire between different cards is a different wire, so remove this one and make that one.","type":"object","properties":{"kind":{"description":"`reference` or `input`.","oneOf":[{"type":"string"},{"type":"null"}]},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"idx":{"description":"Order among the wires sharing a port.","type":"integer"},"props":{"type":"object","additionalProperties":{}},"archived":{"description":"True removes the row from the board, false puts it back. A removal archives rather than deletes, so the row stays readable and a restore can undo it.","type":"boolean"}},"additionalProperties":false}}}},"responses":{"200":{"description":"The link after the change.","content":{"application/json":{"schema":{"title":"Link","description":"A wire between two cards on one board.","type":"object","properties":{"from_id":{"type":"string","format":"uuid"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"id":{"type":"string","format":"uuid"},"kind":{"description":"`input` compiles to a dependency; `reference` is provenance and may form cycles.","type":"string"},"to_port":{"type":"string"},"from_port":{"type":"string"},"idx":{"type":"integer"},"to_id":{"type":"string","format":"uuid"},"scratchpad_id":{"type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","scratchpad_id","from_id","to_id","kind","from_port","to_port","idx","created_at"],"additionalProperties":false}}}},"404":{"description":"No such link.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Somebody else changed it first.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"A field carried a value it does not take.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Scratchpad"],"summary":"Edit or remove a link","description":"Relabel a wire, change whether it is a `reference` or an `input`, or take it off the board with `archived: true`.\n\nA wire's endpoints are fixed: joining two different cards is a different wire, so remove this one and make that one."}},"/api/v1/images":{"get":{"parameters":[{"in":"query","name":"q","required":false,"schema":{"type":"string"},"description":"Natural-language BM25 query, e.g. `\"team working office\"`. Omit for plain browse + filter."},{"in":"query","name":"category","required":false,"schema":{"type":"string"},"description":"Topic filter: agency, ai, automotive, backgrounds, beauty, construction, creative, devtools, ecommerce, education, events, fintech, fitness, food, groceries, healthcare, home-services, logistics, marketing, nonprofit, people, pets, portraits, product, professional, realestate, restaurant, saas, sustainability, travel, wedding, wellness."},{"in":"query","name":"role","required":false,"schema":{"type":"string"},"description":"Placement filter: hero | avatar | thumbnail | background | gallery | logo."},{"in":"query","name":"orientation","required":false,"schema":{"type":"string","enum":["landscape","portrait","square"]},"description":"landscape | portrait | square."},{"in":"query","name":"copy_space","required":false,"schema":{"type":"string","enum":["left","right","top","bottom","center","none"]},"description":"Only images with room for headline text on this side. Use when picking a hero the copy has to sit on."},{"in":"query","name":"limit","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Max rows. 1–100, default 20."},{"in":"query","name":"offset","required":false,"schema":{"type":"integer","minimum":0},"description":"Result offset. Default 0."}],"responses":{"200":{"description":"Matching images, directly placeable.","content":{"application/json":{"schema":{"title":"ImageList","description":"List envelope. `limit`/`offset` echo the query; detect end-of-results when `data.length < limit`.","type":"object","properties":{"data":{"type":"array","items":{"title":"ImageMatch","description":"A search hit, directly placeable: hotlink `src`, render `blurhash` while loading, ALWAYS display `attribution`.","type":"object","properties":{"role":{"description":"Placement hint: hero | avatar | thumbnail | background | gallery | logo.","oneOf":[{"type":"string"},{"type":"null"}]},"description":{"description":"What the photo shows.","oneOf":[{"type":"string"},{"type":"null"}]},"thumb":{"description":"Smaller variant for grids.","oneOf":[{"type":"string"},{"type":"null"}]},"category":{"description":"Topic bucket, e.g. healthcare, saas, realestate, people. One value per topic set — a sub-specialty like dental or legal is searchable via `q`, not here.","oneOf":[{"type":"string"},{"type":"null"}]},"copy_space":{"description":"Where the photo stays flat enough for headline text to sit on it: left | right | top | bottom | center, or none when the frame is busy edge to edge. Put the copy on that side.","oneOf":[{"type":"string"},{"type":"null"}]},"aspect":{"description":"Aspect ratio, e.g. 16:9.","oneOf":[{"type":"string"},{"type":"null"}]},"slug":{"description":"URL-safe handle. The path param on GET /images/{slug}.","type":"string"},"color":{"description":"Dominant hex color — use as the loading background.","oneOf":[{"type":"string"},{"type":"null"}]},"alt":{"description":"Accessibility text.","oneOf":[{"type":"string"},{"type":"null"}]},"width":{"description":"Source pixels.","oneOf":[{"type":"integer"},{"type":"null"}]},"orientation":{"description":"landscape | portrait | square.","oneOf":[{"type":"string"},{"type":"null"}]},"src":{"description":"Hotlink URL (provider CDN). Never mirror or re-host.","oneOf":[{"type":"string"},{"type":"null"}]},"blurhash":{"description":"BlurHash placeholder string for CLS-free loading.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Human-readable title.","oneOf":[{"type":"string"},{"type":"null"}]},"attribution":{"description":"Photographer credit — {photographer, username, profile_url, source_url}. Display wherever the image renders.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"height":{"description":"Source pixels.","oneOf":[{"type":"integer"},{"type":"null"}]}},"required":["slug","title","description","category","role","orientation","aspect","width","height","color","blurhash","copy_space","src","alt","attribution"],"additionalProperties":false}},"limit":{"type":"integer"},"offset":{"type":"integer"}},"required":["data","limit","offset"],"additionalProperties":false}}}}},"tags":["Images"],"summary":"Search and browse the image catalog","description":"Curated stock photography for authored apps — use these instead of placeholder URLs.\n\nSearch semantics:\n  • Pass `q` for BM25 ranking across title, description, and keywords.\n  • Filter by `category` (topic set), `role` (placement hint: hero/avatar/background/...), `orientation`, and `copy_space` — the side of the frame headline text stays legible over. Filter on it whenever the copy sits on the image.\n\nHits are directly placeable: hotlink `src` (never mirror), render `blurhash`/`color` while loading, and ALWAYS display `attribution` near the image per the provider license.\n\nPagination is offset-based; detect end-of-results when `data.length < limit`."}},"/api/v1/orgs/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"}],"responses":{"200":{"description":"The org row.","content":{"application/json":{"schema":{"title":"Organization","description":"Public projection of an organization the caller can see. Mutable fields are exposed via PATCH /api/v1/orgs/{id}.","type":"object","properties":{"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"display_name":{"description":"Human-readable org name shown in UI. Required, 1-200 chars.","type":"string"},"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"image":{"description":"Optional logo URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"settings":{"description":"Free-form org-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"archived":{"description":"True once the org has been archived. Archived orgs remain visible to existing members but can't be PATCHed via this api.","type":"boolean"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"}},"required":["id","display_name","settings","archived","created_at","updated_at"],"additionalProperties":false}}}},"404":{"description":"Org does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Orgs"],"summary":"Get an organization","description":"Returns the org's public metadata.\n\nReturns 404 (not 403) when the caller has no visibility — this prevents probing for existence. If you receive 404 and expected access, list visible orgs with GET /api/v1/orgs to confirm membership."},"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"}],"requestBody":{"content":{"application/json":{"schema":{"title":"OrganizationPatch","description":"Partial-update body. Send only the fields you want to change; omitted fields are preserved. Unknown fields are dropped by coercion before they reach the handler so a client can't accidentally edit protected fields.","type":"object","properties":{"display_name":{"description":"New display name. 1-200 chars.","allOf":[{"type":"string"},{}]},"description":{"description":"New description, or null to clear.","oneOf":[{"type":"string"},{"type":"null"}]},"image":{"description":"New logo URL, or null to clear.","oneOf":[{"type":"string"},{"type":"null"}]},"settings":{"description":"Replaces the entire `settings` map; merge client-side before sending.","type":"object","properties":{},"additionalProperties":false}},"additionalProperties":false}}}},"responses":{"200":{"description":"The updated org row.","content":{"application/json":{"schema":{"title":"Organization","description":"Public projection of an organization the caller can see. Mutable fields are exposed via PATCH /api/v1/orgs/{id}.","type":"object","properties":{"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"display_name":{"description":"Human-readable org name shown in UI. Required, 1-200 chars.","type":"string"},"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"image":{"description":"Optional logo URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"settings":{"description":"Free-form org-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"archived":{"description":"True once the org has been archived. Archived orgs remain visible to existing members but can't be PATCHed via this api.","type":"boolean"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"}},"required":["id","display_name","settings","archived","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"Org does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Orgs"],"summary":"Update an organization","description":"Updates `display_name`, `description`, `image`, or `settings`.\n\nAuthorization: the caller's role must grant `:write :org` (owner or admin). Member or viewer roles get 403.\n\nUnknown body fields are silently dropped by request coercion before they reach the handler, so it is safe to PATCH with the full GET response after editing only the fields you care about.\n\nArchive/delete are not exposed in v1 — they will land on dedicated verbs."}},"/api/v1/executions/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Execution id from POST /api/v1/workspaces/{id}/executions"}],"responses":{"200":{"description":"The execution row.","content":{"application/json":{"schema":{"title":"Execution","description":"One workflow run — the uniform async-job shape. Poll until `status` turns terminal, or tail /events.","type":"object","properties":{"workspace_id":{"description":"The workflow workspace this run executed.","type":"string","format":"uuid"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"app_id":{"description":"Owning app id.","type":"string","format":"uuid"},"updated_at":{"description":"RFC 3339 timestamp.","oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"running | succeeded | failed | cancelled.","type":"string"},"result":{"description":"The workflow's result once status=succeeded. Null while running."},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"error":{"description":"Structured failure ({node_id, message}) once status=failed.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"seq":{"description":"Latest event's seq — resume /events with ?after_seq= from here.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["id","org_id","app_id","workspace_id","status","seq","created_at","updated_at"],"additionalProperties":false}}}},"404":{"description":"Execution does not exist, or the caller has no visibility.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Executions"],"summary":"Get an execution","description":"The run's current state — uniform async-job shape. `result` is set once `status=succeeded`; `error` once `failed`. `seq` is the latest event's address — resume /events with `?after_seq=` from here.\n\nAuthorization: any active member of the run's org (read-only — viewer allowed). 404 opacity for non-members."}},"/api/v1/orgs/{id}/agents/{agent_id}":{"delete":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"},{"in":"path","name":"agent_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Agent member id from GET /api/v1/orgs/{id}/agents"}],"responses":{"204":{"description":"Agent revoked."},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Agents"],"summary":"Revoke an agent","description":"Sets the agent member's status to `revoked`; its token stops authenticating immediately. Authorization: `[:manage :member]` + `[:manage :token]` (owner/admin)."}},"/api/v1/me":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"object","properties":{"active_account_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"email":{"type":"string"},"first_name":{"type":"string"},"settings":{"oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"updated_at":{"type":"string"},"id":{"type":"string","format":"uuid"},"active_org_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"last_name":{"oneOf":[{"type":"string"},{"type":"null"}]},"image":{"oneOf":[{"type":"string"},{"type":"null"}]},"created_at":{"type":"string"}},"required":["id","email","first_name","settings","created_at","updated_at"],"additionalProperties":false}}}},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Me"],"summary":"Get the calling profile","description":"Returns the profile associated with the calling token. Requires a profile-scoped token."},"patch":{"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"first_name":{"type":"string"},"last_name":{"oneOf":[{"type":"string"},{"type":"null"}]},"image":{"oneOf":[{"type":"string"},{"type":"null"}]},"settings":{"type":"object","properties":{},"additionalProperties":false}},"additionalProperties":false}}}},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object","properties":{"active_account_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"email":{"type":"string"},"first_name":{"type":"string"},"settings":{"oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"updated_at":{"type":"string"},"id":{"type":"string","format":"uuid"},"active_org_id":{"oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"last_name":{"oneOf":[{"type":"string"},{"type":"null"}]},"image":{"oneOf":[{"type":"string"},{"type":"null"}]},"created_at":{"type":"string"}},"required":["id","email","first_name","settings","created_at","updated_at"],"additionalProperties":false}}}},"403":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Me"],"summary":"Update the calling profile","description":"Update first_name, last_name, image, or settings on the caller's profile."}},"/api/v1/apps/{app_id}/deployments":{"get":{"parameters":[{"in":"path","name":"app_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps. The collection lists deployments owned by this app."}],"responses":{"200":{"description":"Deployments for the app.","content":{"application/json":{"schema":{"title":"DeploymentList","description":"Paginated-ready envelope. v1 returns the entire visible set in `data`; future revisions may add pagination cursors alongside.","type":"object","properties":{"data":{"type":"array","items":{"title":"Deployment","description":"Public projection of an app deployment. One row per (app, env) pair; phase + stage transition over the row's lifetime via the studio deploy flow.","type":"object","properties":{"slug":{"description":"Vanity label the app also serves on, as <slug>.orbitapps.ai. Unique across the platform; unlike a custom domain it needs no DNS record and no certificate. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"stage":{"description":"Sub-state when phase=building: bundle | apply | wait-ready. Nil otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"issues":{"description":"What the last deploy could not build. Empty on a clean deploy. The app still serves — phase stays \"deployed\" — but the named parts are missing, so their routes and schedules are not mounted.","type":"array","items":{"title":"DeploymentIssue","description":"One part of the app the last deploy could not build.","type":"object","properties":{"kind":{"description":"What kind of thing failed to build, e.g. \"workflow\".","oneOf":[{"type":"string"},{"type":"null"}]},"workspace_id":{"description":"Workspace that holds it, so a client can link straight to the editor.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Its name, as the author sees it in Studio.","oneOf":[{"type":"string"},{"type":"null"}]},"reason":{"description":"Grep-able rule name, e.g. \"workflow-preflight-failed\".","oneOf":[{"type":"string"},{"type":"null"}]},"message":{"description":"What went wrong, already written for the author.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["kind","workspace_id","title","reason","message"],"additionalProperties":false}},"started_at":{"description":"RFC 3339 timestamp the last deploy attempt began. May be null on a never-deployed row.","oneOf":[{"type":"string"},{"type":"null"}]},"completed_at":{"description":"RFC 3339 timestamp the last deploy attempt reached a terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"events":{"description":"Per-transition timeline (sliding window of last 50). Drives the UI timeline panel.","type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"ip_address":{"description":"Provider-assigned IP, when surfaced. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"app_id":{"description":"Owning app id. Immutable: deployments cannot be reassigned to another app.","type":"string","format":"uuid"},"history":{"description":"Per-attempt deploy history (sliding window of last 20). Drives the Versions list + revert.","type":"array","items":{"title":"DeploymentHistoryEntry","description":"One per-attempt summary; powers the Versions list + Revert.","type":"object","properties":{"at":{"description":"RFC 3339 timestamp the attempt reached terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_url":{"description":"Bundle pointer in R2 — passed back via deploy to revert to this version.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_version":{"description":"Short slug from the bundle path.","oneOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"description":"Profile id (uuid string) of whoever started the attempt. Nil for reconciler-driven transitions.","oneOf":[{"type":"string"},{"type":"null"}]},"outcome":{"description":"deployed | degraded | failed | stopped. \"degraded\" means the release went live but could not build every part — see the deployment's issues.","oneOf":[{"type":"string"},{"type":"null"}]},"error_code":{"description":"Machine-readable failure code on failed/stopped outcomes; nil on deployed.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["at","bundle_url","bundle_version","triggered_by","outcome","error_code"],"additionalProperties":false}},"phase":{"description":"High-level lifecycle phase. One of: idle | building | deployed | stopping | stopped | failed.","type":"string"},"public_url":{"description":"Publicly reachable URL once the deployment is live. May be null while building / failed.","oneOf":[{"type":"string"},{"type":"null"}]},"env":{"description":"Environment slot — one of \"dev\", \"uat\", or \"prod\". Each (app, env) pair has at most one deployment row.","type":"string"},"last_heartbeat_at":{"description":"RFC 3339 timestamp of the last successful watcher/reconciler tick. Drives staleness detection.","oneOf":[{"type":"string"},{"type":"null"}]},"provider_app_id":{"description":"Identifier returned by the hosting provider after the first successful deploy.","oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"description":"RFC 3339 timestamp of the most recent transition.","type":"string"},"status":{"description":"Uniform async-job status derived from phase: pending | running | succeeded | cancelled | failed. The same enum every long-running resource exposes — poll generically on this.","type":"string"},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"bundle_url":{"description":"R2 manifest URL of the bundle currently deployed. Used by revert.","oneOf":[{"type":"string"},{"type":"null"}]},"error":{"description":"Structured failure context (source / code / detail / at). Non-null only when phase=failed.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"bundle_version":{"description":"Short slug for the bundle currently deployed.","oneOf":[{"type":"string"},{"type":"null"}]},"domain":{"description":"Custom domain bound to this deployment, when configured. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"version":{"description":"Monotonic counter, bumped on every transition. Useful as a cache-invalidation key.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp. Stable across phase transitions.","type":"string"}},"required":["id","app_id","org_id","env","phase","status","issues","events","history","version","created_at","updated_at"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both, with the same opacity as /apps).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"List an app's deployments","description":"Returns every deployment the calling token can read for the path's app.\n\nVisibility: same model as GET /api/v1/apps/{id}. An active member of the app's org sees its deployments regardless of role. Non-members get 404 with the `app-not-found` spec — same Stripe-style opacity as /apps reads.\n\nEach (app, env) pair has at most one deployment row. Status transitions over the row's lifetime (idle → building → deployed, or → failed / stopped) via the studio deploy flow.\n\nUse the returned id with the flat single-deployment subtree at /api/v1/deployments/{id} for read / restart / stop / refresh / logs."},"post":{"parameters":[{"in":"path","name":"app_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps. The collection lists deployments owned by this app."}],"requestBody":{"content":{"application/json":{"schema":{"title":"DeploymentTrigger","description":"Request body for POST /api/v1/apps/{app_id}/deployments. Everything is optional — the default is a free-tier dev launch.","type":"object","properties":{"env":{"description":"Target environment. One deployment row exists per (app, env). Defaults to `dev` — a prod launch must be asked for by name.","type":"string","enum":["dev","uat","prod"]},"tier":{"description":"Hosting tier. Omitted/`free` launches at $0 (one free deployment per org); paid tiers place a prepaid hold on the org's wallet.","type":"string","enum":["free","hobby","starter","growth","scale"]},"force":{"description":"When true and this $0 launch hits the org's single free slot already held by another (app, env), stop that occupant first and take the slot instead of returning 409 free-slot-taken. Tears down the running app; only affects free-tier launches.","type":"boolean"},"plan_hash":{"description":"Approval token for a disruptive schema change. Only needed when a prior trigger returned 409 deployment-plan-approval-required — set it to that response's `plan.hash` to approve exactly those changes. Safe/additive deploys need no plan_hash.","type":"string"},"slug":{"description":"Vanity label to serve this app on, as <slug>.orbitapps.ai, alongside its default host. One lowercase DNS label, unique across the platform. Claimed as part of the launch, so a first deploy comes up on a readable URL.","type":"string"}},"additionalProperties":false}}}},"responses":{"202":{"description":"The deployment row at phase=building. Poll the Location header until a terminal phase.","content":{"application/json":{"schema":{"title":"Deployment","description":"Public projection of an app deployment. One row per (app, env) pair; phase + stage transition over the row's lifetime via the studio deploy flow.","type":"object","properties":{"slug":{"description":"Vanity label the app also serves on, as <slug>.orbitapps.ai. Unique across the platform; unlike a custom domain it needs no DNS record and no certificate. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"stage":{"description":"Sub-state when phase=building: bundle | apply | wait-ready. Nil otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"issues":{"description":"What the last deploy could not build. Empty on a clean deploy. The app still serves — phase stays \"deployed\" — but the named parts are missing, so their routes and schedules are not mounted.","type":"array","items":{"title":"DeploymentIssue","description":"One part of the app the last deploy could not build.","type":"object","properties":{"kind":{"description":"What kind of thing failed to build, e.g. \"workflow\".","oneOf":[{"type":"string"},{"type":"null"}]},"workspace_id":{"description":"Workspace that holds it, so a client can link straight to the editor.","oneOf":[{"type":"string"},{"type":"null"}]},"title":{"description":"Its name, as the author sees it in Studio.","oneOf":[{"type":"string"},{"type":"null"}]},"reason":{"description":"Grep-able rule name, e.g. \"workflow-preflight-failed\".","oneOf":[{"type":"string"},{"type":"null"}]},"message":{"description":"What went wrong, already written for the author.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["kind","workspace_id","title","reason","message"],"additionalProperties":false}},"started_at":{"description":"RFC 3339 timestamp the last deploy attempt began. May be null on a never-deployed row.","oneOf":[{"type":"string"},{"type":"null"}]},"completed_at":{"description":"RFC 3339 timestamp the last deploy attempt reached a terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"events":{"description":"Per-transition timeline (sliding window of last 50). Drives the UI timeline panel.","type":"array","items":{"title":"DeploymentEvent","description":"One transition record from the deployment's sub-deploy timeline.","type":"object","properties":{"seq":{"description":"Monotonic event address (= the row's version at transition time). Resume with ?after_seq= / SSE Last-Event-ID. 0 for events recorded before seq stamping.","type":"integer"},"at":{"description":"RFC 3339 timestamp of the transition.","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"phase-transition | stage-transition | error | user-action","oneOf":[{"type":"string"},{"type":"null"}]},"from":{"description":"Previous state subset (phase + stage).","type":"object","additionalProperties":{}},"to":{"description":"New state subset.","type":"object","additionalProperties":{}},"reason":{"description":"Short grep-able keyword like 'bundle-uploaded' or 'watcher-deadline'.","oneOf":[{"type":"string"},{"type":"null"}]},"detail":{"description":"Optional human context for the transition.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["seq","at","type","from","to","reason","detail"],"additionalProperties":false}},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"ip_address":{"description":"Provider-assigned IP, when surfaced. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"app_id":{"description":"Owning app id. Immutable: deployments cannot be reassigned to another app.","type":"string","format":"uuid"},"history":{"description":"Per-attempt deploy history (sliding window of last 20). Drives the Versions list + revert.","type":"array","items":{"title":"DeploymentHistoryEntry","description":"One per-attempt summary; powers the Versions list + Revert.","type":"object","properties":{"at":{"description":"RFC 3339 timestamp the attempt reached terminal phase.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_url":{"description":"Bundle pointer in R2 — passed back via deploy to revert to this version.","oneOf":[{"type":"string"},{"type":"null"}]},"bundle_version":{"description":"Short slug from the bundle path.","oneOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"description":"Profile id (uuid string) of whoever started the attempt. Nil for reconciler-driven transitions.","oneOf":[{"type":"string"},{"type":"null"}]},"outcome":{"description":"deployed | degraded | failed | stopped. \"degraded\" means the release went live but could not build every part — see the deployment's issues.","oneOf":[{"type":"string"},{"type":"null"}]},"error_code":{"description":"Machine-readable failure code on failed/stopped outcomes; nil on deployed.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["at","bundle_url","bundle_version","triggered_by","outcome","error_code"],"additionalProperties":false}},"phase":{"description":"High-level lifecycle phase. One of: idle | building | deployed | stopping | stopped | failed.","type":"string"},"public_url":{"description":"Publicly reachable URL once the deployment is live. May be null while building / failed.","oneOf":[{"type":"string"},{"type":"null"}]},"env":{"description":"Environment slot — one of \"dev\", \"uat\", or \"prod\". Each (app, env) pair has at most one deployment row.","type":"string"},"last_heartbeat_at":{"description":"RFC 3339 timestamp of the last successful watcher/reconciler tick. Drives staleness detection.","oneOf":[{"type":"string"},{"type":"null"}]},"provider_app_id":{"description":"Identifier returned by the hosting provider after the first successful deploy.","oneOf":[{"type":"string"},{"type":"null"}]},"updated_at":{"description":"RFC 3339 timestamp of the most recent transition.","type":"string"},"status":{"description":"Uniform async-job status derived from phase: pending | running | succeeded | cancelled | failed. The same enum every long-running resource exposes — poll generically on this.","type":"string"},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"bundle_url":{"description":"R2 manifest URL of the bundle currently deployed. Used by revert.","oneOf":[{"type":"string"},{"type":"null"}]},"error":{"description":"Structured failure context (source / code / detail / at). Non-null only when phase=failed.","oneOf":[{"type":"object","properties":{},"additionalProperties":false},{"type":"null"}]},"bundle_version":{"description":"Short slug for the bundle currently deployed.","oneOf":[{"type":"string"},{"type":"null"}]},"domain":{"description":"Custom domain bound to this deployment, when configured. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"version":{"description":"Monotonic counter, bumped on every transition. Useful as a cache-invalidation key.","type":"integer"},"created_at":{"description":"RFC 3339 timestamp. Stable across phase transitions.","type":"string"}},"required":["id","app_id","org_id","env","phase","status","issues","events","history","version","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to deploy.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both, with the same opacity as /apps).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Insufficient credits for the tier, the org's free slot is taken, or the migration plan needs approval (body carries `plan`; re-POST with `plan_hash`).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"The migration plan is blocked by unexecutable schema changes (body carries `plan`). Fix the data model and redeploy.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Trigger a deploy","description":"Launches (or relaunches) the app on `env` and answers 202 immediately with the deployment row at phase=`building`, stage=`bundle`. The bundle upload + provider deploy run in the background; every transition is persisted onto the row (`phase`, `stage`, `events`), so poll GET /api/v1/deployments/{id} (the `Location` header) until phase reaches `deployed` (the row then carries `public_url`) or `failed` (see `error`).\n\nBilling: paid tiers require the org's wallet to cover one month at the tier's rate — otherwise 409 `insufficient-credits`. A free-tier launch is refused with 409 `free-slot-taken` while another (app, env) holds the org's single free slot.\n\nAuthorization: `[:write :resource]` (owner/admin/member) on the app's org; 404 opacity for non-members.\n\nSchema changes: if the app's data model changed since the last deploy, the trigger computes a migration plan. A safe/additive plan proceeds silently. A disruptive plan (a warning cast, or a removed field/table that will be skipped so data is preserved) is refused with 409 `deployment-plan-approval-required` carrying the `plan` — review it, then re-POST with `plan_hash` = `plan.hash` to approve. A plan with unexecutable changes is refused with 422 `deployment-plan-blocked` and is never approvable.\n\nSupports the `Idempotency-Key` header — a retried trigger replays the original 202 instead of launching a second flow."}},"/api/v1/apps/{app_id}/workspaces":{"get":{"parameters":[{"in":"path","name":"app_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"},{"in":"query","name":"kind","required":false,"schema":{"type":"string","enum":["mockup","workflow","entity"]},"description":"Workspace kind. `mockup` for UI trees, `workflow` for backend logic, `entity` for user-defined data models."}],"responses":{"200":{"description":"Workspaces owned by the app.","content":{"application/json":{"schema":{"title":"AppWorkspaceList","description":"Workspaces owned by the parent app.","type":"object","properties":{"data":{"type":"array","items":{"title":"WorkspaceSummary","description":"Slim workspace pointer returned under an app. Fetch GET /api/v1/workspaces/{id} for the full node tree.","type":"object","properties":{"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Workspace kind.","type":"string"},"title":{"description":"Human-readable name.","type":"string"},"archived":{"description":"True once the workspace has been archived.","type":"boolean"},"root_node_id":{"description":"Convenience pointer to the tree root. Null when the workspace has not been seeded yet.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"created_at":{"description":"RFC 3339 timestamp.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp.","type":"string"}},"required":["id","kind","title","archived","root_node_id","created_at","updated_at"],"additionalProperties":false}}},"required":["data"],"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"List the app's workspaces","description":"Returns every workspace owned by `app_id` the caller can see. Visibility mirrors GET /api/v1/apps/{id} — the caller must be an active member of the app's org; otherwise 404 carrying the `app-not-found` spec (Stripe-style opacity, no role leak).\n\nFilter by kind with `?kind=mockup|workflow|entity`. Omit to list all kinds.\n\nThis endpoint returns the slim workspace pointer shape — no nodes. Use the returned `id` with GET /api/v1/workspaces/{id} to fetch the flat node list."},"post":{"parameters":[{"in":"path","name":"app_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"}],"requestBody":{"content":{"application/json":{"schema":{"title":"WorkspaceCreate","description":"Request body for POST /api/v1/apps/{app_id}/workspaces. Creates the workspace and seeds its root node (mockup → Page, workflow → Workflow + start/end children, entity → DataModel).","type":"object","properties":{"kind":{"description":"Workspace kind. `mockup` for UI trees, `workflow` for backend logic, `entity` for user-defined data models.","type":"string","enum":["mockup","workflow","entity"]},"title":{"description":"Human-readable name. 1-200 chars.","allOf":[{"type":"string"},{}]},"props":{"description":"Props for the seeded root node. Mockup pages typically want a `slug` prop (e.g. {\"slug\": \"/about\"}) so the deployed router can mount them. Defaults to {}.","type":"object","additionalProperties":{}}},"required":["kind","title"],"additionalProperties":false}}}},"responses":{"201":{"description":"The created workspace plus its seeded nodes.","content":{"application/json":{"schema":{"title":"Workspace","description":"Workspace plus its flat normalized node list.","type":"object","properties":{"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"archived":{"description":"True once archived.","type":"boolean"},"org_id":{"description":"Owning org id.","type":"string","format":"uuid"},"root_node_id":{"description":"Pointer to the seeded root node.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"app_id":{"description":"Owning app id.","type":"string","format":"uuid"},"settings":{"description":"Free-form workspace settings map.","type":"object","additionalProperties":{}},"title":{"description":"Human-readable name.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp.","type":"string"},"nodes":{"description":"Every node, flat, sorted by (parent_id, idx, id).","type":"array","items":{"title":"Node","description":"One normalized node row. Rebuild the hierarchy from `parent_id`/`idx`.","type":"object","properties":{"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"parent_id":{"description":"Parent node id. Null for the root node.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"idx":{"description":"Sibling ordering. 0-based, ascending.","type":"integer"},"component":{"description":"Renderer dispatch key or workflow node type.","type":"string"},"props":{"description":"Per-node prop map.","type":"object","additionalProperties":{}},"hidden":{"description":"Hidden from the canvas without being archived.","type":"boolean"},"archived":{"description":"Soft-deleted.","type":"boolean"}},"required":["id","parent_id","idx","component","props","hidden","archived"],"additionalProperties":false}},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Workspace kind.","type":"string"},"created_at":{"description":"RFC 3339 timestamp.","type":"string"}},"required":["id","org_id","app_id","kind","title","settings","archived","root_node_id","created_at","updated_at","nodes"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"The app already has an entity workspace.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"Create a workspace under the app","description":"Creates a workspace of the requested kind and seeds its root node:\n\n  • `mockup`   → a `Page` root carrying the body `props` (set a `slug` prop so the deployed router can mount the page, e.g. {\"slug\": \"/about\"}).\n  • `workflow` → a `Workflow` root plus pre-wired `start` and `end` child nodes — author your steps between them with POST /api/v1/workspaces/{id}/nodes.\n  • `entity`   → a `DataModel` root. An app holds exactly one entity workspace; a second create returns 409 carrying the `entity-workspace-exists` spec.\n\nAuthorization: active member row in the app's org with a role granting `[:write :resource]` (owner, admin, or member). Viewer is rejected with 403.\n\nResponds with the same shape as GET /api/v1/workspaces/{id} — flat `nodes` included — so every seeded node id is immediately addressable."}},"/api/v1/cards/{id}":{"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Card id."}],"requestBody":{"content":{"application/json":{"schema":{"title":"CardPatch","description":"Partial update. Send only what changes; an omitted field is left alone and a field carrying null is cleared.\n\nA card guards its content and its layout on separate revisions, so editing text and moving the card are two writes that cannot clobber one another — which is what lets a person drag a card while an agent rewrites it.","type":"object","properties":{"y":{"type":"number"},"archived":{"description":"True removes the row from the board, false puts it back. A removal archives rather than deletes, so the row stays readable and a restore can undo it.","type":"boolean"},"content":{"description":"Kind-specific payload, replaced wholesale.","type":"object","additionalProperties":{}},"width":{"oneOf":[{"type":"number"},{"type":"null"}]},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"z":{"type":"integer"},"status":{"description":"`draft` | `ready` | `failed` | `accepted`.","oneOf":[{"type":"string"},{"type":"null"}]},"x":{"type":"number"},"height":{"oneOf":[{"type":"number"},{"type":"null"}]},"props":{"description":"Presentation options, replaced wholesale.","type":"object","additionalProperties":{}}},"additionalProperties":false}}}},"responses":{"200":{"description":"The card after the change.","content":{"application/json":{"schema":{"title":"Card","description":"One card on the board.","type":"object","properties":{"y":{"type":"number"},"content_url":{"description":"Where a file card's bytes are served, when they are public. Null otherwise.","oneOf":[{"type":"string"},{"type":"null"}]},"content":{"description":"Kind-specific payload. An `image` or `file` card carries `artifact_id`, `filename` and `mime_type`.","type":"object","additionalProperties":{}},"width":{"oneOf":[{"type":"number"},{"type":"null"}]},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"z":{"type":"integer"},"status":{"description":"`draft` | `ready` | `failed` | `accepted`.","oneOf":[{"type":"string"},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier.","type":"string","format":"uuid"},"kind":{"description":"Renderer family: `spark` | `text` | `image` | `file` | `website` | `preview` | `idea` | `hill` | `artifact`.","type":"string"},"x":{"type":"number"},"artifact_kind":{"description":"Structured outcome an artifact card holds: `insights`. Unrelated to a stored file.","oneOf":[{"type":"string"},{"type":"null"}]},"scratchpad_id":{"description":"Owning board id.","type":"string","format":"uuid"},"created_at":{"oneOf":[{"type":"string"},{"type":"null"}]},"height":{"oneOf":[{"type":"number"},{"type":"null"}]},"props":{"description":"Presentation options that are not the material itself.","type":"object","additionalProperties":{}}},"required":["id","scratchpad_id","kind","content","props","x","y","z","created_at"],"additionalProperties":false}}}},"404":{"description":"No such card.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"409":{"description":"Somebody else changed it first.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"A field carried a value it does not take.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Scratchpad"],"summary":"Edit or remove a card","description":"Change what a card holds, where it sits, or both. Send only the fields that change.\n\nContent and layout advance on separate revisions, so an edit to a card's text and a drag of the same card are two writes that do not collide — one call may carry both.\n\n`archived: true` removes the card from the board and `false` restores it. Nothing is deleted, so a removal is recoverable and the wires that touched the card are archived with it."}},"/api/v1/apps/{app_id}/preview.png":{"get":{"parameters":[{"in":"path","name":"app_id","required":true,"schema":{"type":"string","format":"uuid"}},{"in":"query","name":"page","required":false,"schema":{"type":"string"},"description":"Page slug to render (e.g. \"/pricing\"). Defaults to the app's home page."},{"in":"query","name":"width","required":false,"schema":{"type":"integer","minimum":200,"maximum":3840},"description":"Viewport width in CSS px (default 1280)."},{"in":"query","name":"height","required":false,"schema":{"type":"integer","minimum":200,"maximum":3840},"description":"Viewport height in CSS px (default 800)."},{"in":"query","name":"dpr","required":false,"schema":{"type":"integer","minimum":1,"maximum":3},"description":"Device scale factor / retina multiplier (default 1)."},{"in":"query","name":"full_page","required":false,"schema":{"type":"boolean"},"description":"Capture a layout overview of the whole page (default false). Bounded to 1280x8000 at DPR 1."}],"responses":{"200":{"description":"PNG screenshot (image/png)."},"404":{"description":"App not found, or no renderable page."},"502":{"description":"Screenshot service returned an invalid image."},"503":{"description":"Screenshot service not configured."}},"tags":["Preview"],"summary":"Render an app to a PNG screenshot","description":"Renders the app's LIVE design-time state (the pages as currently authored — not the last deployed bundle) to a PNG so an agent can SEE how it looks. Returns `image/png` bytes.\n\nPass `?page=<slug>` to pick a page (defaults to the home page) and `?width=&height=&dpr=` to control the viewport. Pass `?full_page=true` for a layout overview capped at width 1280 and height 8000 — a capture that tall is downscaled hard before a model reads it, so use the default first-fold shot to read text. The image re-renders on each request, so it always reflects the current state."}},"/api/v1/apps/{id}":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"}],"responses":{"200":{"description":"The app row.","content":{"application/json":{"schema":{"title":"App","description":"Public projection of an app the caller can see. Mutable fields are exposed via PATCH /api/v1/apps/{id}.","type":"object","properties":{"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"idea_id":{"description":"Linked idea id, when the app was scaffolded from an idea. May be null.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"archived":{"description":"True once the app has been archived. Archive/delete verbs are not yet exposed in v1.","type":"boolean"},"slug":{"description":"URL-safe identifier, unique within the org. Lowercase letters, digits, and hyphens; 1-60 chars.","type":"string"},"org_id":{"description":"Owning org id. Immutable: apps cannot be transferred between orgs via this api.","type":"string","format":"uuid"},"settings":{"description":"Free-form app-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"icon":{"description":"Single-glyph emoji shown alongside the title. Defaults to ✨.","oneOf":[{"type":"string"},{"type":"null"}]},"themes":{"description":"Theme catalog: {active, definitions}. Drives end-user theming in the canvas.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"title":{"description":"Human-readable name shown in UI. 1-200 chars.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"},"preview":{"description":"Optional preview image URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"locales":{"description":"The languages this app serves: `active` (the one it is authored in), `available` (the others it offers) and `glossary` (names a translator must not translate). Read what is translated, and run the translator, at GET /api/v1/apps/{app_id}/translations.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"}},"required":["id","org_id","slug","title","icon","archived","settings","themes","locales","created_at","updated_at"],"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"Get an app","description":"Returns the app's public metadata.\n\nReturns 404 (not 403) when the caller has no visibility into the app's org — this prevents probing for existence. If you receive 404 and expected access, list visible apps with GET /api/v1/apps to confirm membership."},"patch":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps"}],"requestBody":{"content":{"application/json":{"schema":{"title":"AppPatch","description":"Partial-update body. Send only the fields you want to change; omitted fields are preserved. Unknown fields are dropped by coercion before they reach the handler so a client can't accidentally edit protected fields (id, org_id, slug, archived).","type":"object","properties":{"title":{"description":"New title. 1-200 chars.","allOf":[{"type":"string"},{}]},"icon":{"description":"New icon glyph, or null to clear (server falls back to ✨).","oneOf":[{"type":"string"},{"type":"null"}]},"description":{"description":"New description, or null to clear.","oneOf":[{"type":"string"},{"type":"null"}]},"preview":{"description":"New preview URL, or null to clear.","oneOf":[{"type":"string"},{"type":"null"}]},"settings":{"description":"Replaces the entire `settings` map; merge client-side before sending.","type":"object","additionalProperties":{}},"themes":{"description":"Replaces the entire `themes` map; merge client-side before sending.","type":"object","additionalProperties":{}},"locales":{"description":"MERGES over the stored `locales` map, unlike `settings` and `themes`, which replace. Its keys are written by different surfaces — `active` when the app is created, `available` when someone adds a language, `glossary` in the panel — so sending one must not erase the others. Send `{\"available\": [\"fr\", \"pt-BR\"]}` to set the languages the app offers. Each code is BCP-47 in canonical case (`fr`, `pt-BR`, `zh-Hans`); the authored language is dropped from the list, and any unusable code is refused with 422 rather than quietly skipped.","type":"object","additionalProperties":{}}},"additionalProperties":false}}}},"responses":{"200":{"description":"The updated app row.","content":{"application/json":{"schema":{"title":"App","description":"Public projection of an app the caller can see. Mutable fields are exposed via PATCH /api/v1/apps/{id}.","type":"object","properties":{"description":{"description":"Optional short blurb. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"idea_id":{"description":"Linked idea id, when the app was scaffolded from an idea. May be null.","oneOf":[{"type":"string","format":"uuid"},{"type":"null"}]},"archived":{"description":"True once the app has been archived. Archive/delete verbs are not yet exposed in v1.","type":"boolean"},"slug":{"description":"URL-safe identifier, unique within the org. Lowercase letters, digits, and hyphens; 1-60 chars.","type":"string"},"org_id":{"description":"Owning org id. Immutable: apps cannot be transferred between orgs via this api.","type":"string","format":"uuid"},"settings":{"description":"Free-form app-level settings map. Treat as opaque to other tools.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"icon":{"description":"Single-glyph emoji shown alongside the title. Defaults to ✨.","oneOf":[{"type":"string"},{"type":"null"}]},"themes":{"description":"Theme catalog: {active, definitions}. Drives end-user theming in the canvas.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"title":{"description":"Human-readable name shown in UI. 1-200 chars.","type":"string"},"updated_at":{"description":"RFC 3339 timestamp of the most recent PATCH or system mutation.","type":"string"},"preview":{"description":"Optional preview image URL. May be null.","oneOf":[{"type":"string"},{"type":"null"}]},"locales":{"description":"The languages this app serves: `active` (the one it is authored in), `available` (the others it offers) and `glossary` (names a translator must not translate). Read what is translated, and run the translator, at GET /api/v1/apps/{app_id}/translations.","oneOf":[{"type":"object","additionalProperties":{}},{"type":"null"}]},"id":{"description":"Stable UUIDv7 identifier. Use this in path params.","type":"string","format":"uuid"},"created_at":{"description":"RFC 3339 timestamp. Stable across PATCHes.","type":"string"}},"required":["id","org_id","slug","title","icon","archived","settings","themes","locales","created_at","updated_at"],"additionalProperties":false}}}},"403":{"description":"Caller has visibility but lacks the role to write.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}},"422":{"description":"A language code in `locales.available` is not usable; nothing was stored.","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Apps"],"summary":"Update an app","description":"Updates `title`, `icon`, `description`, `preview`, `settings`, `themes`, or `locales`.\n\nAuthorization: the caller must have an active member row in the app's org and a role granting `[:write :resource]` (owner, admin, or member). Viewer is rejected with 403.\n\nUnknown body fields are silently dropped by request coercion before they reach the handler, so it is safe to PATCH with the full GET response after editing only the fields you care about. Protected fields (id, org_id, slug, archived, created_at, updated_at) cannot be modified via this endpoint.\n\n`locales` is the exception to the replace rule: it MERGES over the stored map, because its keys are written by different surfaces. Send `locales: {\"available\": [\"fr\"]}` to set the languages the app offers, then translate it at POST /api/v1/apps/{app_id}/translations.\n\nArchive/delete are not exposed in v1 — they will land on dedicated verbs."}},"/api/v1/apps/{app_id}/deployments/plan":{"get":{"parameters":[{"in":"path","name":"app_id","required":true,"schema":{"type":"string","format":"uuid"},"description":"App id from GET /api/v1/apps. The collection lists deployments owned by this app."},{"in":"query","name":"env","required":false,"schema":{"type":"string","enum":["dev","uat","prod"]},"description":"Environment to plan for. One of dev | uat | prod. Defaults to prod."}],"responses":{"200":{"description":"The migration plan for the app/env.","content":{"application/json":{"schema":{"title":"MigrationPlan","description":"The schema changes a deploy would make to the app's database — the diff of the live data model against the last successful deploy. Additive-only: removed fields/tables are `skip`ped (data preserved), never dropped. Obey `disruptive?`/`blocked?` rather than re-deriving policy.","type":"object","properties":{"hash":{"description":"Content hash of the steps. Pass back as `plan_hash` to approve a disruptive deploy.","type":"string"},"blocked?":{"description":"True when any step is unexecutable — the deploy is refused until the data model is fixed.","type":"boolean"},"database?":{"description":"False for a pages-only app with no data model (empty plan).","type":"boolean"},"first_deploy?":{"description":"True when no stored bundle carries a schema to diff against — every table is a fresh create. A deploy that shipped no entities (runtime \"static\") is not a schema checkpoint, so it does not clear this.","type":"boolean"},"generated_at":{"description":"RFC 3339 timestamp the plan was computed.","oneOf":[{"type":"string"},{"type":"null"}]},"steps":{"description":"Every diff step, in execution order.","type":"array","items":{"title":"MigrationPlanStep","description":"One schema change in the migration plan.","type":"object","properties":{"description":{"description":"One-line human description of the change.","type":"string"},"schema":{"description":"Physical schema name.","type":"string"},"table":{"description":"Table the change targets (nil for schema-level steps).","oneOf":[{"type":"string"},{"type":"null"}]},"type":{"description":"DDL operation, e.g. create-table, add-column, drop-column, alter-column-type.","type":"string"},"warnings":{"description":"Human warnings the diff engine attached to this step.","type":"array","items":{"type":"string"}},"column":{"description":"Column the change targets, when applicable.","type":"string"},"sql":{"description":"The SQL this step would run.","type":"string"},"id":{"description":"Stable step id (type + schema + table + column).","type":"string"},"safety":{"description":"safe: harmless · warning: applied but may lose data · destructive: a drop (skipped) · unexecutable: cannot run (blocks).","type":"string","enum":["safe","warning","destructive","unexecutable"]},"action":{"description":"apply: will run · skip: a drop, never run (data preserved) · block: unexecutable, refuses the deploy.","type":"string","enum":["apply","skip","block"]}},"required":["id","type","schema","table","safety","action","sql","description"],"additionalProperties":false}},"summary":{"description":"Step counts by action + total warnings.","type":"object","properties":{"apply":{"type":"integer"},"skip":{"type":"integer"},"block":{"type":"integer"},"warnings":{"type":"integer"}},"required":["apply","skip","block","warnings"],"additionalProperties":false},"disruptive?":{"description":"True when the plan warns, skips a destructive change, or blocks — the trigger then requires plan_hash approval.","type":"boolean"},"source_bundle_version":{"description":"Bundle slug the plan diffed against; nil on the first deploy.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["hash","steps","summary","disruptive?","blocked?"],"additionalProperties":false}}}},"404":{"description":"App does not exist, or the caller has no visibility (response shape is intentionally identical for both, with the same opacity as /apps).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Deployments"],"summary":"Preview a deploy's schema changes","description":"The migration plan a deploy to `env` would run — the diff of the app's live data model against the schema of its last successful deploy — without deploying.\n\nAdditive-only: removed fields/tables are `skip`ped (their data is preserved), never dropped. Obey `disruptive?` and `blocked?` rather than re-deriving policy from the steps.\n\nRead-only. Same visibility as the list endpoint: an active member of the app's org sees the plan; non-members get 404 `app-not-found` opacity.\n\nThis is a convenience for reviewing changes ahead of time — the deploy trigger recomputes the plan inline, so you never have to call this before deploying."}},"/api/v1/orgs/{id}/wallet":{"get":{"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string","format":"uuid"},"description":"Org id from GET /api/v1/orgs"}],"responses":{"200":{"description":"The wallet position. Zeros with null currency/status when the org has no wallet yet.","content":{"application/json":{"schema":{"title":"Wallet","description":"The org's prepaid credit position, in cents.","type":"object","properties":{"balance":{"description":"Cash on hand, before live AI-meter consumption.","type":"integer"},"usable":{"description":"Balance minus unsettled meter cost — what's actually spendable right now.","type":"integer"},"refundable":{"description":"Unused PAID principal — bonus credits are never refundable.","type":"integer"},"capacity":{"description":"The wallet's 'full mark' for progress displays.","type":"integer"},"currency":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"description":"active | suspended | closed. Null when the org has no wallet yet.","oneOf":[{"type":"string"},{"type":"null"}]},"dunning_since":{"description":"When usable went negative; null while healthy.","oneOf":[{"type":"string"},{"type":"null"}]}},"required":["balance","usable","refundable","capacity"],"additionalProperties":false}}}},"404":{"description":"Org does not exist, or the caller has no visibility (intentionally identical shapes).","content":{"application/json":{"schema":{"type":"object","properties":{},"additionalProperties":false}}}}},"tags":["Billing"],"summary":"Get the org's wallet","description":"The prepaid credit position, all in cents: `balance` (cash on hand), `usable` (balance minus unsettled meter cost — what a deploy's affordability check sees), and `refundable` (unused paid principal; bonus credits never refund).\n\nTop-ups happen in Studio — this surface is read-only.\n\nAuthorization: any active member (viewer included)."}},"/api/v1/health":{"get":{"responses":{"200":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok","degraded","down"]},"timestamp":{"type":"string"}},"required":["status","timestamp"],"additionalProperties":false}}}}},"tags":["Health"],"summary":"Health check","description":"Returns the health status of the API. Used by load balancers and orchestrators.","security":[]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"opaque","description":"An Orbit api token, sent as `Authorization: Bearer <token>`.\n\nMint one with the RFC 8628 device-authorization grant (`POST /api/v1/device/code`, then poll `POST /api/v1/device/token`), or from Studio.\n\nEvery token carries a scope set from the `[verb resource]` RBAC catalog — `read:app`, `write:deployment`, `manage:secret`, and so on. The full vocabulary is published as `scopes_supported` at `/.well-known/oauth-protected-resource` (RFC 9728) and `/.well-known/oauth-authorization-server` (RFC 8414). Grant an agent the narrowest set that does its job."}}},"servers":[{"url":"https://api.orbitapps.io","description":"Production"}],"info":{"title":"Orbit Apps API","version":"1.0.0","description":"Public REST API for Orbit Apps."},"x-id":["reitit.openapi/default"],"openapi":"3.1.0"}