{"openapi":"3.1.0","info":{"title":"CranL API","version":"1.0.0","description":"Deploy and run applications, databases, email, storage and sandboxes on CranL. Every endpoint is also an MCP tool at /api/mcp with the same name as its operationId."},"servers":[{"url":"https://app.cranl.com"}],"tags":[{"name":"core","description":"Account, regions, projects, repositories, docs and operation tracking."},{"name":"apps","description":"Create, inspect, configure and delete applications."},{"name":"deployments","description":"Deploy, wait, roll back, and diagnose failed builds."},{"name":"observability","description":"Logs, requests, metrics and health checks."},{"name":"env","description":"Environment variables with history."},{"name":"domains","description":"Custom domains and CDN cache."},{"name":"databases","description":"Managed databases: create, connect to apps, query, inspect schema."},{"name":"email","description":"Send email and manage sending domains and templates."},{"name":"storage","description":"S3-compatible buckets and access keys."},{"name":"cdn","description":"Standalone CDN zones."},{"name":"sandboxes","description":"Cloud machines for agents: create one, run commands and code, read and write files, test and screenshot what it serves."},{"name":"sandbox_extras","description":"More sandbox tools: one-shot runs, processes, logs, search, resize, checkpoints, pull requests and spend."},{"name":"cloud_agents","description":"Cloud Agents: run Claude Code, Codex, OpenCode and other coding agents on a task, from a prompt to a pull request. Follow their work, message them, compare them."},{"name":"cloud_agent_setup","description":"Cloud Agents setup: model keys, environments with warm snapshots, automations (GitHub, schedules, webhooks, Slack), workspaces and settings."},{"name":"realtime","description":"Realtime: live channels, presence and history over WebSockets. Create apps, publish, sign client tokens, watch channels and wire keys into apps."},{"name":"queues","description":"Queues and workers: durable messages with retries, dead letters, delays, ordering and fan-out; push to an endpoint or pull from a worker; schedules, inboxes for webhooks, and workers that scale with the backlog."},{"name":"backends","description":"Managed backends (Supabase or Convex)."},{"name":"previews","description":"Preview deployments of a branch."},{"name":"cron","description":"Scheduled commands and persistent volumes."},{"name":"blueprints","description":"Describe infrastructure in cranl.json; plan and apply it."},{"name":"events","description":"Events, webhooks and uptime monitors: be told instead of polling."},{"name":"account","description":"Activity log and account-level settings."}],"paths":{"/api/v1/whoami":{"get":{"operationId":"whoami","summary":"Who am I","description":"The user, the account this credential acts on, and what the credential may do: its scopes, and limited_to when it only reaches named apps and databases. Call first when unsure what is allowed.","tags":["core"],"x-cranl-scope":null,"x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"}},"required":["id","email"],"additionalProperties":false},"account":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"credential":{"type":"object","properties":{"kind":{"type":"string"},"name":{"anyOf":[{"type":"string"},{"type":"null"}]},"scopes":{"type":"array","items":{"type":"string"}},"expires_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"limited_to":{"anyOf":[{"type":"object","properties":{"apps":{"type":"array","items":{"type":"string"}},"databases":{"type":"array","items":{"type":"string"}}},"required":["apps","databases"],"additionalProperties":false},{"type":"null"}]}},"required":["kind","name","scopes","expires_at","limited_to"],"additionalProperties":false}},"required":["user","account","credential"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/account":{"get":{"operationId":"get_account","summary":"Account, plan, limits and usage","description":"The account's plan, its limits and current usage (e.g. 1/2 apps & databases), which regions it may deploy to, which features it includes, and the upgrade link. Check this before creating things so you plan within limits instead of failing into them.","tags":["core"],"security":[{"bearer":[]},{"oauth2":["account:read"]}],"x-cranl-scope":"account:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"anyOf":[{"type":"string"},{"type":"null"}]},"locked":{"type":"boolean"},"limits":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"usage":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"features":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}},"regions_available":{"type":"array","items":{"type":"string"}},"upgrade_url":{"type":"string"}},"required":["plan","locked","limits","usage","features","regions_available","upgrade_url"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/regions":{"get":{"operationId":"list_regions","summary":"List regions","description":"Every deploy region with its slug (pass it as `region`), location, and whether this account may use it. Ask the user which region to use when it matters; put databases in the same region as their app.","tags":["core"],"x-cranl-scope":null,"x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"regions":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"group":{"type":"string"},"country":{"type":"string"},"available":{"type":"boolean"},"available_to_you":{"type":"boolean"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["slug","name","group","country","available","available_to_you","reason"],"additionalProperties":false}}},"required":["regions"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/projects":{"get":{"operationId":"list_projects","summary":"List projects","description":"Projects in this account. Most operations default to the oldest project when project_id is omitted.","tags":["core"],"security":[{"bearer":[]},{"oauth2":["projects:read"]}],"x-cranl-scope":"projects:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"projects":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}}},"required":["projects"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_project","summary":"Create project","description":"Create a project to group apps and databases.","tags":["core"],"security":[{"bearer":[]},{"oauth2":["projects:write"]}],"x-cranl-scope":"projects:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/github/repos":{"get":{"operationId":"list_github_repos","summary":"List GitHub repositories","description":"Repositories from the GitHub accounts connected to this CranL account (refreshed from GitHub first). Use the `id` or `full_name` as create_app's `repository`. If `connected` is false, the user must connect GitHub in the browser at connect_url.","tags":["core"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"openWorld":true},"parameters":[{"name":"project_id","in":"query","required":false,"schema":{"description":"Project to look through (defaults to the oldest).","type":"string"},"description":"Project to look through (defaults to the oldest)."},{"name":"search","in":"query","required":false,"schema":{"description":"Only repositories whose name contains this.","type":"string"},"description":"Only repositories whose name contains this."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"connected":{"type":"boolean"},"connect_url":{"type":"string"},"repositories":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"full_name":{"type":"string"},"default_branch":{"anyOf":[{"type":"string"},{"type":"null"}]},"language":{"anyOf":[{"type":"string"},{"type":"null"}]},"private":{"anyOf":[{"type":"boolean"},{"type":"null"}]}},"required":["id","full_name","default_branch","language","private"],"additionalProperties":false}}},"required":["connected","connect_url","repositories"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/activity":{"get":{"operationId":"list_activity","summary":"Activity log","description":"What was changed in this account through keys and agents, newest first: which operation, on what, by which credential and client, and whether it succeeded.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["account:read"]}],"x-cranl-scope":"account:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"before","in":"query","required":false,"schema":{"description":"ISO timestamp; entries older than this.","type":"string"},"description":"ISO timestamp; entries older than this."},{"name":"target_id","in":"query","required":false,"schema":{"description":"Only entries about this resource.","type":"string"},"description":"Only entries about this resource."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"entries":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["entries"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/docs/search":{"get":{"operationId":"search_docs","summary":"Search CranL docs","description":"Search CranL's documentation for how to do something (deploying without Git, custom domains, cache headers, errors, scopes…). Prefer this over guessing.","tags":["core"],"x-cranl-scope":null,"x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"What you want to know."},"description":"What you want to know."},{"name":"topic","in":"query","required":false,"schema":{"description":"Return one topic in full: getting-started, authentication, deploy-github, deploy-source, deployments, regions, databases, env, domains, previews, observability, email, sandboxes, cloud-agents, realtime, queues, blueprints, safety, backups, no-account, events, reliability, database-safety, control, workflow, account-admin, errors.","type":"string"},"description":"Return one topic in full: getting-started, authentication, deploy-github, deploy-source, deployments, regions, databases, env, domains, previews, observability, email, sandboxes, cloud-agents, realtime, queues, blueprints, safety, backups, no-account, events, reliability, database-safety, control, workflow, account-admin, errors."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","properties":{"slug":{"type":"string"},"title":{"type":"string"},"summary":{"type":"string"},"body":{"type":"string"}},"required":["slug","title","summary","body"],"additionalProperties":false}}},"required":["results"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/operations/{operation_id}":{"get":{"operationId":"get_operation","summary":"Get operation","description":"The current state of a long-running operation (a deploy, a database starting). Does not wait.","tags":["core"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"operation_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Operation id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/operations/{operation_id}/wait":{"post":{"operationId":"wait_operation","summary":"Wait for operation","description":"Wait until an operation finishes (or timeout_seconds passes) and return its final state. On a failed deploy the result carries the log excerpt, an AI fix, and next_actions.","tags":["core"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"operation_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Operation id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"timeout_seconds":{"description":"Default 300.","type":"integer","minimum":1,"maximum":900}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/projects/{project_id}/graph":{"get":{"operationId":"get_project_graph","summary":"Get project graph","description":"Everything in a project and how it connects, in one call: apps (with their source, address and last deploy), databases, buckets, sandboxes, queues, and the CDN zones in front of them as nodes; which app uses which database or bucket, which zone serves what, which app is a preview of which, and which apps send to or handle each queue as edges. Resources the credential may not read (apps:read, db:read, storage:read, cdn:read, sandboxes:read, queues:read) are left out.","tags":["core"],"security":[{"bearer":[]},{"oauth2":["projects:read"]}],"x-cranl-scope":"projects:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Project id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"project":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false},"nodes":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"kind:id, as edges refer to it"},"kind":{"type":"string","enum":["app","database","bucket","cdn","sandbox","queue"]},"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"region":{"anyOf":[{"type":"string"},{"type":"null"}]},"protected":{"type":"boolean"}},"required":["key","kind","id","name","status","region","protected"],"additionalProperties":{}}},"edges":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"kind":{"type":"string","enum":["database","bucket","cdn","preview"],"description":"database/bucket: the app uses it; cdn: the zone serves it; preview: a preview of the app"},"source":{"type":"string"},"target":{"type":"string"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"For a database edge, the variable that holds its connection URL"}},"required":["key","kind","source","target","label"],"additionalProperties":false}}},"required":["project","nodes","edges"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps":{"get":{"operationId":"list_apps","summary":"List applications","description":"Applications in this account with status, URL, region and source. Previews and apps scheduled for deletion are marked.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"project_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"include_deleted","in":"query","required":false,"schema":{"description":"Also list apps scheduled for deletion.","type":"boolean"},"description":"Also list apps scheduled for deletion."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"apps":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}}}},"required":["apps"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_app","summary":"Create application from Git","description":"Create an app from a connected GitHub repository (`repository`: numeric id or \"owner/name\" from list_github_repos) or any public Git URL (`git_url`), then deploy it. Returns the app and a deploy operation; pass wait: true to return when the deploy finishes. For code on disk without a repository, use deploy_source instead.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63,"description":"App name; also its <name>.cranl.net address."},"region":{"type":"string","description":"Region slug from list_regions, e.g. netherlands-1."},"repository":{"description":"GitHub repository id or owner/name.","type":"string"},"git_url":{"description":"A public Git URL, when the repo is not connected.","type":"string","format":"uri"},"branch":{"description":"Default: main.","type":"string"},"build":{"description":"auto detects the framework (default); dockerfile uses your Dockerfile.","type":"string","enum":["auto","dockerfile"]},"dockerfile_path":{"type":"string"},"build_path":{"description":"Subdirectory to build from (monorepos). Default /.","type":"string"},"port":{"description":"Port the app listens on. Default 3000. Ignored for workers.","type":"integer","minimum":1,"maximum":65535},"kind":{"description":"worker: runs all the time with no public address, e.g. a queue consumer (no domain, no PORT; scale it with update_worker). Default web.","type":"string","enum":["web","worker"]},"project_id":{"type":"string"},"description":{"type":"string"},"env":{"description":"Environment variables to set before the first boot, e.g. { \"NODE_ENV\": \"production\" }.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"wait":{"description":"Return only when the first deploy finishes (default false).","type":"boolean"},"timeout_seconds":{"description":"How long to wait (default 300).","type":"integer","minimum":10,"maximum":900},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","region"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}},"operation":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false}},"required":["app","operation"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}":{"get":{"operationId":"get_app","summary":"Get application","description":"One application: status, URL, region, source (repository/branch or image), port, custom domains, preview parent.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"update_app","summary":"Update application","description":"Change the port the app listens on. Takes effect on the next deploy or restart.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"port":{"type":"integer","minimum":1,"maximum":65535},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["port"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}}},"required":["app"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_app","summary":"Delete application","description":"Stop the app and schedule it for deletion in 7 days. Until then restore_app brings it back unchanged, and it still counts toward the plan's app limit. Destructive: needs a confirmation token.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:delete"]}],"x-cranl-scope":"apps:delete","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deletion_scheduled_at":{"type":"string"},"restore_with":{"type":"string"}},"required":["ok","deletion_scheduled_at","restore_with"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/docker":{"post":{"operationId":"create_docker_app","summary":"Create application from a Docker image","description":"Run a Docker image (public, or private with registry credentials). Deploys immediately.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"region":{"type":"string"},"image":{"type":"string","description":"e.g. nginx:1.27 or ghcr.io/org/app:tag"},"port":{"type":"integer","minimum":1,"maximum":65535},"kind":{"description":"worker: runs all the time with no public address, e.g. a queue consumer (no domain, no PORT; scale it with update_worker). Default web.","type":"string","enum":["web","worker"]},"command":{"type":"string"},"registry_username":{"type":"string"},"registry_password":{"type":"string"},"volumes":{"description":"Container paths to persist across deploys, e.g. [\"/data\"].","type":"array","items":{"type":"string"}},"project_id":{"type":"string"},"env":{"description":"Environment variables to set before the first boot, e.g. { \"NODE_ENV\": \"production\" }.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"wait":{"description":"Return only when the first deploy finishes (default false).","type":"boolean"},"timeout_seconds":{"description":"How long to wait (default 300).","type":"integer","minimum":10,"maximum":900},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","region","image"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}},"operation":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false}},"required":["app","operation"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/static":{"post":{"operationId":"create_static_site","summary":"Create static site","description":"Create a static site served from the CDN at https://<subdomain>.cranl.net. Upload its files with deploy_source (kind: static), or pass archive_base64 there directly.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"subdomain":{"type":"string","minLength":2,"maxLength":40,"description":"Letters, digits and dashes."},"spa":{"description":"Serve index.html for unknown paths (single-page apps).","type":"boolean"},"project_id":{"type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","subdomain"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}}},"required":["app"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/templates":{"get":{"operationId":"list_templates","summary":"List one-click app templates","description":"Open-source apps deployable in one call (WordPress, Odoo, Mattermost…), with the options each takes.","tags":["apps"],"x-cranl-scope":null,"x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"templates":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["templates"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/templates/deploy":{"post":{"operationId":"deploy_template","summary":"Deploy a template","description":"Deploy a one-click app (see list_templates). Creates its database too when it needs one; that counts toward the plan's limit.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"template":{"type":"string","description":"Template id from list_templates."},"region":{"type":"string"},"name":{"type":"string"},"options":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"project_id":{"type":"string"},"wait":{"description":"Return only when the first deploy finishes (default false).","type":"boolean"},"timeout_seconds":{"description":"How long to wait (default 300).","type":"integer","minimum":10,"maximum":900},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["template","region"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}},"database_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"operation":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false}},"required":["app","database_id","operation"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/lifecycle":{"post":{"operationId":"app_lifecycle","summary":"Start, stop, restart or rebuild","description":"restart applies new env vars without rebuilding; rebuild rebuilds from the current source; stop and start do what they say.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:deploy"]}],"x-cranl-scope":"apps:deploy","x-cranl-annotations":{},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["start","stop","restart","rebuild","reload"]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["action"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"action":{"type":"string"},"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}}},"required":["ok","action","app"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/restore":{"post":{"operationId":"restore_app","summary":"Restore a deleted application","description":"Cancel a scheduled deletion and start the app again.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}}},"required":["app"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/deploy":{"post":{"operationId":"deploy_app","summary":"Deploy application","description":"Build and deploy the app's current source (latest commit on its branch, or the last uploaded archive). Returns an operation. With wait: true the final result comes back in the same call; a failure includes the failing log lines, an AI-suggested fix and next_actions. Refuses to start a second deploy while one is running.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:deploy"]}],"x-cranl-scope":"apps:deploy","x-cranl-annotations":{},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"wait":{"description":"Return when the deploy finishes (default false). On failure the result includes the log excerpt, an AI fix and next_actions.","type":"boolean"},"timeout_seconds":{"description":"How long to wait (default 300).","type":"integer","minimum":10,"maximum":900},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"operation":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false}},"required":["operation"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/deployments":{"get":{"operationId":"list_deployments","summary":"List deployments","description":"Recent deployments of an app, newest first, with status (queued, building, succeeded, failed) and the failure reason.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"deployments":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["deployments"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/deployments/{deployment_id}/logs":{"get":{"operationId":"get_deployment_logs","summary":"Build logs","description":"Build log of one deployment (ids from list_deployments). around_error: true returns only the lines around the first error (best first call on a failure); tail and grep narrow the rest. While the build is still running it returns what has been logged so far with in_progress: true: call again for more.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"deployment_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Deployment id"}},{"name":"around_error","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"tail","in":"query","required":false,"schema":{"description":"Last N lines (default 200).","type":"integer","minimum":1,"maximum":2000},"description":"Last N lines (default 200)."},{"name":"grep","in":"query","required":false,"schema":{"description":"Only lines containing this (case-insensitive).","type":"string"},"description":"Only lines containing this (case-insensitive)."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"lines":{"type":"array","items":{"type":"string"}},"first_error":{"anyOf":[{"type":"string"},{"type":"null"}]},"truncated":{"type":"boolean"},"in_progress":{"type":"boolean"}},"required":["lines","first_error","truncated","in_progress"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/deployments/{deployment_id}/ai-fix":{"get":{"operationId":"get_ai_fix","summary":"AI fix for a failed deployment","description":"Analyse a failed deployment's build log and source and return a concrete fix: file edits (search/replace or new content) and env changes. Apply the edits, then deploy_source or push, and deploy_app with wait: true.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true,"openWorld":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"deployment_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Deployment id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"fix":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]}},"required":["fix"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/deployments/{deployment_id}/diagnosis":{"get":{"operationId":"diagnose_deployment","summary":"Diagnose a deployment","description":"Everything about one failed deployment in one call: first error, log excerpt, AI fix and next_actions.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true,"openWorld":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"deployment_id","in":"path","required":true,"schema":{"description":"Default: the latest deployment.","type":"string","minLength":1}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/rollback":{"post":{"operationId":"rollback_app","summary":"Roll back","description":"Return the app to an earlier successful deployment without rebuilding: to: \"previous\" (the one before the current) or a deployment id. Returns an operation.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:deploy"]}],"x-cranl-scope":"apps:deploy","x-cranl-annotations":{},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"to":{"description":"\"previous\" (default) or a deployment id from list_deployments.","type":"string"},"wait":{"description":"Return when the deploy finishes (default false). On failure the result includes the log excerpt, an AI fix and next_actions.","type":"boolean"},"timeout_seconds":{"description":"How long to wait (default 300).","type":"integer","minimum":10,"maximum":900},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"operation":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false},"rolled_back_to":{"type":"string"}},"required":["operation","rolled_back_to"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/uploads":{"post":{"operationId":"create_upload","summary":"Create upload URL","description":"Reserve a one-time URL for a source archive (.tar.gz, .tar or .zip, up to 100 MB). PUT the file there (curl -T app.tar.gz \"<upload_url>\"), then call deploy_source with upload_id. For archives under 8 MB, pass archive_base64 to deploy_source instead.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:deploy"]}],"x-cranl-scope":"apps:deploy","x-cranl-annotations":{},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"upload_id":{"type":"string"},"upload_url":{"type":"string"},"method":{"type":"string","const":"PUT"},"expires_in":{"type":"number"}},"required":["upload_id","upload_url","method","expires_in"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/deploy-source":{"post":{"operationId":"deploy_source","summary":"Deploy local source (no Git)","description":"Deploy code from the agent's working directory. Send the directory as an archive (archive_base64 under 8 MB, or upload_id from create_upload). kind \"app\" builds and runs it like a Git app (framework detected, or your Dockerfile); kind \"static\" serves the files as-is from the CDN (upload the build output, e.g. dist/); every file is public, so .env files, key files (*.pem, *.key, id_rsa…), dotfiles other than .well-known/ and a top-level cranl.json are never published and come back in skipped_files. Pass app_id to redeploy an existing app, or name (+ region for apps) to create one. Returns the app and an operation; wait: true returns when live.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:deploy"]}],"x-cranl-scope":"apps:deploy","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"description":"Default app.","type":"string","enum":["app","static"]},"app_id":{"type":"string"},"name":{"type":"string","minLength":1,"maxLength":63},"region":{"description":"Required when creating an app of kind app.","type":"string"},"archive_base64":{"type":"string"},"upload_id":{"type":"string"},"build":{"type":"string","enum":["auto","dockerfile"]},"build_path":{"description":"Subdirectory inside the archive to build (default: root).","type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"spa":{"description":"Static sites: serve index.html for unknown paths.","type":"boolean"},"project_id":{"type":"string"},"env":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"wait":{"description":"Return when the deploy finishes (default false). On failure the result includes the log excerpt, an AI fix and next_actions.","type":"boolean"},"timeout_seconds":{"description":"How long to wait (default 300).","type":"integer","minimum":10,"maximum":900},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}},"operation":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false},{"type":"null"}]},"files":{"type":"number"},"skipped_files":{"description":"Static sites: paths in the archive that were not published because they are private (env files, keys, dotfiles, a top-level cranl.json).","type":"array","items":{"type":"string"}},"private_files":{"description":"Static sites: private files an earlier publish left live on the site. deploy_source only adds and overwrites files, so they stay live until the owner publishes a new version from the site's settings in the dashboard, which deletes every file not in that upload. Tell the user, and that any secrets in those files need rotating.","type":"array","items":{"type":"string"}},"warning":{"description":"Static sites: what private_files means, in words for the user.","type":"string"},"preflight":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["app","operation","files"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/logs":{"get":{"operationId":"get_app_logs","summary":"Application logs","description":"Runtime logs of a running app (latest container). Narrow with since (\"15m\"), grep, level (\"error\" or \"warn\") and tail; summary: true returns the first error, error count and most frequent messages instead of lines.","tags":["observability"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"since","in":"query","required":false,"schema":{"type":"string"}},{"name":"grep","in":"query","required":false,"schema":{"type":"string"}},{"name":"level","in":"query","required":false,"schema":{"description":"Minimum level.","type":"string","enum":["error","warn","info"]},"description":"Minimum level."},{"name":"tail","in":"query","required":false,"schema":{"description":"Default 100.","type":"integer","minimum":1,"maximum":200},"description":"Default 100."},{"name":"summary","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/errors":{"get":{"operationId":"get_errors","summary":"Runtime errors","description":"Errors in the app's runtime logs grouped by kind, each with how often it happened, first and last time, an example message and its stack trace, and whether it is new since the last deploy. Answers \"what broke since the deploy?\" in one call.","tags":["observability"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"since","in":"query","required":false,"schema":{"description":"15m, 2h, 1d or a timestamp. Default: everything in the current logs.","type":"string"},"description":"15m, 2h, 1d or a timestamp. Default: everything in the current logs."},{"name":"only_new","in":"query","required":false,"schema":{"description":"Only errors first seen after the latest deploy went live.","type":"boolean"},"description":"Only errors first seen after the latest deploy went live."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/requests":{"get":{"operationId":"query_requests","summary":"Query HTTP requests","description":"Individual HTTP requests served for the app, newest first. Filter by status (500, >=500, 4xx), path (contains), method, country and since. summary: true returns totals, error rate, slowest paths and top failing paths.","tags":["observability"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"path","in":"query","required":false,"schema":{"type":"string"}},{"name":"method","in":"query","required":false,"schema":{"type":"string"}},{"name":"country","in":"query","required":false,"schema":{"type":"string","minLength":2,"maxLength":2}},{"name":"since","in":"query","required":false,"schema":{"description":"Default 1h.","type":"string"},"description":"Default 1h."},{"name":"limit","in":"query","required":false,"schema":{"description":"Default 50.","type":"integer","minimum":1,"maximum":200},"description":"Default 50."},{"name":"summary","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/metrics":{"get":{"operationId":"get_app_metrics","summary":"Application metrics","description":"CPU, memory, disk and network for the running app: the latest reading and a short recent series.","tags":["observability"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"points","in":"query","required":false,"schema":{"description":"Series length (default 30).","type":"integer","minimum":1,"maximum":120},"description":"Series length (default 30)."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/health-check":{"post":{"operationId":"check_app_health","summary":"Check the app responds","description":"Fetch a path on the app (through its public URL) and check the status and, optionally, that the body (its first 2 MB) contains some text. Use after a deploy to prove the app works before telling the user it does.","tags":["observability"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true,"openWorld":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"description":"Default /.","type":"string"},"host":{"description":"One of the app's custom domains (default: its cranl.net address).","type":"string"},"expect_status":{"description":"Default: any 2xx or 3xx.","type":"integer","minimum":100,"maximum":599},"expect_text":{"type":"string"},"timeout_seconds":{"type":"integer","minimum":1,"maximum":30}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"passed":{"type":"boolean"},"status":{"anyOf":[{"type":"number"},{"type":"null"}]},"latency_ms":{"anyOf":[{"type":"number"},{"type":"null"}]},"url":{"type":"string"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}]},"body_excerpt":{"anyOf":[{"type":"string"},{"type":"null"}]},"content_type":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["passed","status","latency_ms","url","reason","body_excerpt","content_type"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/env":{"get":{"operationId":"get_env","summary":"Get environment variables","description":"The app's environment variables. Values are masked unless reveal: true (needs the env:reveal scope). Returns a version to pass as expected_version when saving.","tags":["env"],"security":[{"bearer":[]},{"oauth2":["env:read"]}],"x-cranl-scope":"env:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"reveal","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string"},"variables":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"},"masked":{"type":"boolean"}},"required":["key","value","masked"],"additionalProperties":false}}},"required":["version","variables"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"set_env","summary":"Set environment variables","description":"Add or change variables (others are kept). Pass expected_version from get_env to refuse the save if someone else changed them meanwhile; restart: true applies them immediately. Without the env:reveal scope the reply only confirms that the save happened: version is null and changed lists every variable passed, whether or not its value differed.","tags":["env"],"security":[{"bearer":[]},{"oauth2":["env:write"]}],"x-cranl-scope":"env:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"},"description":"e.g. { \"DATABASE_URL\": \"postgres://…\" }"},"expected_version":{"type":"string"},"restart":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["variables"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The saved environment's version; null without \"env:reveal\""},"restarted":{"type":"boolean"},"changed":{"type":"array","items":{"type":"string"},"description":"Variables whose value differed; without \"env:reveal\", every variable passed"},"note":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["version","restarted","changed","note"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/env/unset":{"post":{"operationId":"unset_env","summary":"Remove environment variables","description":"Remove variables by name. Accepts expected_version and restart like set_env.","tags":["env"],"security":[{"bearer":[]},{"oauth2":["env:write"]}],"x-cranl-scope":"env:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"minItems":1,"type":"array","items":{"type":"string"}},"expected_version":{"type":"string"},"restart":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["keys"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The saved environment's version; null without \"env:reveal\""},"restarted":{"type":"boolean"},"removed":{"type":"array","items":{"type":"string"}},"note":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["version","restarted","removed","note"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/env/versions":{"get":{"operationId":"list_env_versions","summary":"Environment history","description":"Previous saves of the app's environment (which variables existed, when, and who or what changed them).","tags":["env"],"security":[{"bearer":[]},{"oauth2":["env:read"]}],"x-cranl-scope":"env:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"versions":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["versions"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/env/versions/{version_id}/restore":{"post":{"operationId":"restore_env_version","summary":"Restore an environment version","description":"Replace the app's environment with a previous version from list_env_versions. Destructive: needs a confirmation token.","tags":["env"],"security":[{"bearer":[]},{"oauth2":["env:write"]}],"x-cranl-scope":"env:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"version_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Version id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"restart":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The saved environment's version; null without \"env:reveal\""},"restarted":{"type":"boolean"},"note":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["version","restarted","note"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/domains":{"get":{"operationId":"list_domains","summary":"List domains","description":"The app's cranl.net address and custom domains, with SSL status for each.","tags":["domains"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"default":{"anyOf":[{"type":"string"},{"type":"null"}]},"domains":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["default","domains"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"add_domain","summary":"Add custom domain","description":"Put the app on the user's own domain. Returns the DNS record the user must create (a CNAME to the app's cranl.net address). SSL is issued once DNS resolves; check with list_domains, retry with enable_domain_ssl.","tags":["domains"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true,"openWorld":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"host":{"type":"string","description":"e.g. www.example.com"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["host"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"host":{"type":"string"},"dns":{"type":"object","properties":{"type":{"type":"string"},"name":{"type":"string"},"value":{"type":"string"}},"required":["type","name","value"],"additionalProperties":false},"ssl":{"type":"string"}},"required":["host","dns","ssl"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/domains/{domain_id}":{"delete":{"operationId":"remove_domain","summary":"Remove custom domain","description":"Detach a custom domain from the app. Destructive: needs a confirmation token.","tags":["domains"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"domain_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Domain (from list_domains) id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/domains/ssl":{"post":{"operationId":"enable_domain_ssl","summary":"Issue SSL for a domain","description":"Retry issuing the SSL certificate for a custom domain once its DNS points at the app.","tags":["domains"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true,"openWorld":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"host":{"type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["host"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/purge-cache":{"post":{"operationId":"purge_cache","summary":"Purge CDN cache","description":"Clear everything the CDN has cached for the app. Prefer fixing Cache-Control headers for dynamic responses (search_docs \"caching\"); purge after a content change that must show immediately.","tags":["domains"],"security":[{"bearer":[]},{"oauth2":["apps:deploy"]}],"x-cranl-scope":"apps:deploy","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases":{"get":{"operationId":"list_databases","summary":"List databases","description":"Managed databases in this account with type, status and region.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:read"]}],"x-cranl-scope":"db:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"project_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"include_deleted","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"databases":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"status":{"type":"string"},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","type","status","region"],"additionalProperties":{}}}},"required":["databases"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_database","summary":"Create database","description":"Create a managed PostgreSQL, MySQL, MariaDB, MongoDB or Redis database. Put it in the same region as the app that uses it, then call connect_database_to_app. Counts toward the plan's apps+databases limit.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"type":{"type":"string","enum":["postgresql","mysql","mariadb","mongodb","redis"]},"region":{"type":"string","description":"Same region as the app, e.g. netherlands-1."},"project_id":{"type":"string"},"database_name":{"type":"string"},"user":{"type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","type","region"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"database":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"status":{"type":"string"},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","type","status","region"],"additionalProperties":{}},"connection_url":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["database","connection_url"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}":{"get":{"operationId":"get_database","summary":"Get database","description":"One database with its internal connection URL (password masked unless reveal: true, which needs env:reveal). Apps in the same region reach it through that URL.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:read"]}],"x-cranl-scope":"db:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}},{"name":"reveal","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"status":{"type":"string"},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","type","status","region"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_database","summary":"Delete database","description":"Stop the database and schedule it for deletion in 7 days; restore_database undoes it until then. Destructive: needs a confirmation token.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:delete"]}],"x-cranl-scope":"db:delete","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"deletion_scheduled_at":{"type":"string"}},"required":["ok","deletion_scheduled_at"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/connect":{"post":{"operationId":"connect_database_to_app","summary":"Connect database to app","description":"Set the database's connection URL on an app as an environment variable (DATABASE_URL by default; REDIS_URL / MONGODB_URL for those types) and restart the app. Refuses when they are in different regions unless allow_cross_region is true. env_version is the saved environment's version, as get_env shows it; it is null without the env:reveal scope.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"app_id":{"type":"string","minLength":1,"description":"Application id"},"variable":{"type":"string"},"restart":{"description":"Default true.","type":"boolean"},"allow_cross_region":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["app_id"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variable":{"type":"string"},"app_id":{"type":"string"},"restarted":{"type":"boolean"},"env_version":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The saved environment's version; null without \"env:reveal\""}},"required":["variable","app_id","restarted","env_version"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/lifecycle":{"post":{"operationId":"database_lifecycle","summary":"Start, stop or restart a database","description":"start, stop, restart (reload) or rebuild a database.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["start","stop","restart","rebuild"]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["action"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"status":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["ok","status"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/restore":{"post":{"operationId":"restore_database","summary":"Restore a deleted database","description":"Cancel a scheduled deletion and start the database again.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"type":{"type":"string"},"status":{"type":"string"},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","type","status","region"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/query":{"post":{"operationId":"query_database","summary":"Query a database","description":"Run a query: SQL for PostgreSQL/MySQL/MariaDB, a JSON command for MongoDB ({\"find\": \"users\", \"filter\": {}, \"limit\": 10}), a command for Redis (GET key). Read-only by default. write: true allows changes; it needs the db:write scope and a confirmation token.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:read"]}],"x-cranl-scope":"db:read","x-cranl-annotations":{},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"query":{"type":"string","minLength":1},"write":{"type":"boolean"},"max_rows":{"description":"Rows returned (default 100).","type":"integer","minimum":1,"maximum":1000},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["query"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/schema":{"get":{"operationId":"get_database_schema","summary":"Database schema","description":"Tables (or collections, or key types) and, for one table, its columns, keys and indexes.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:read"]}],"x-cranl-scope":"db:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}},{"name":"table","in":"query","required":false,"schema":{"description":"Describe this table in full.","type":"string"},"description":"Describe this table in full."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/email":{"get":{"operationId":"get_email_status","summary":"Email status","description":"Whether email is set up for this account, and its sending domains with verification status.","tags":["email"],"security":[{"bearer":[]},{"oauth2":["email:read"]}],"x-cranl-scope":"email:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/email/send":{"post":{"operationId":"send_email","summary":"Send email","description":"Send an email from an address on a verified domain (see get_email_status). Provide html and/or text, or a template_id with variables.","tags":["email"],"security":[{"bearer":[]},{"oauth2":["email:send"]}],"x-cranl-scope":"email:send","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"from":{"type":"string","description":"e.g. \"Acme <hello@acme.com>\"; the domain must be verified."},"to":{"anyOf":[{"type":"string"},{"maxItems":50,"type":"array","items":{"type":"string"}}]},"subject":{"type":"string","minLength":1},"html":{"type":"string"},"text":{"type":"string"},"reply_to":{"type":"string"},"template_id":{"type":"string"},"variables":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"number"}]}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["from","to","subject"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/email/domains":{"get":{"operationId":"list_email_domains","summary":"Email domains","description":"Sending domains with their verification status and the DNS records still needed.","tags":["email"],"security":[{"bearer":[]},{"oauth2":["email:read"]}],"x-cranl-scope":"email:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"domains":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["domains"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"add_email_domain","summary":"Add email domain","description":"Add a sending domain. Returns the DNS records (SPF, DKIM, DMARC) the user must create before verify_email_domain.","tags":["email"],"security":[{"bearer":[]},{"oauth2":["email:send"]}],"x-cranl-scope":"email:send","x-cranl-annotations":{"openWorld":true},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["domain"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/email/domains/{domain_id}/verify":{"post":{"operationId":"verify_email_domain","summary":"Verify email domain","description":"Check the domain's DNS records now and update its status.","tags":["email"],"security":[{"bearer":[]},{"oauth2":["email:send"]}],"x-cranl-scope":"email:send","x-cranl-annotations":{"idempotent":true,"openWorld":true},"parameters":[{"name":"domain_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Email domain id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/email/templates":{"get":{"operationId":"list_email_templates","summary":"Email templates","description":"Saved templates usable as send_email's template_id.","tags":["email"],"security":[{"bearer":[]},{"oauth2":["email:read"]}],"x-cranl-scope":"email:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/email/history":{"get":{"operationId":"list_email_history","summary":"Sent email history","description":"Recently sent emails with delivery status (delivered, bounced, opened…).","tags":["email"],"security":[{"bearer":[]},{"oauth2":["email:read"]}],"x-cranl-scope":"email:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/buckets":{"get":{"operationId":"list_buckets","summary":"List storage buckets","description":"S3-compatible buckets in this account with their public CDN URL.","tags":["storage"],"security":[{"bearer":[]},{"oauth2":["storage:read"]}],"x-cranl-scope":"storage:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"buckets":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["buckets"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_bucket","summary":"Create storage bucket","description":"Create an S3-compatible bucket (Pro and Enterprise). Then create_bucket_key for credentials.","tags":["storage"],"security":[{"bearer":[]},{"oauth2":["storage:write"]}],"x-cranl-scope":"storage:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":3,"maxLength":30},"project_id":{"type":"string"},"region_hint":{"type":"string","enum":["auto","wnam","enam","weur","eeur","apac"]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/buckets/{bucket_id}/keys":{"post":{"operationId":"create_bucket_key","summary":"Create bucket access key","description":"S3 credentials for a bucket. The secret is shown only once; store it (e.g. set_env) straight away.","tags":["storage"],"security":[{"bearer":[]},{"oauth2":["storage:write"]}],"x-cranl-scope":"storage:write","x-cranl-annotations":{},"parameters":[{"name":"bucket_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Bucket id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"permission":{"type":"string","enum":["read-write","read-only"]},"label":{"type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/buckets/{bucket_id}/objects":{"get":{"operationId":"list_bucket_objects","summary":"List bucket objects","description":"Objects under a prefix, with a continuation token for the next page.","tags":["storage"],"security":[{"bearer":[]},{"oauth2":["storage:read"]}],"x-cranl-scope":"storage:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"bucket_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Bucket id"}},{"name":"prefix","in":"query","required":false,"schema":{"type":"string"}},{"name":"token","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"operationId":"upload_object","summary":"Upload a file to a bucket","description":"Store a file in a bucket under key (e.g. \"images/logo.png\"). Pass text for text files or content_base64 for anything else; up to 10 MB. An existing file with the same key is replaced.","tags":["storage"],"security":[{"bearer":[]},{"oauth2":["storage:write"]}],"x-cranl-scope":"storage:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"bucket_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Bucket id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string","minLength":1,"maxLength":1024},"text":{"type":"string"},"content_base64":{"type":"string"},"content_type":{"description":"Default: guessed as application/octet-stream, or text/plain for text.","type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["key"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"key":{"type":"string"},"size":{"type":"number"}},"required":["ok","key","size"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cdn-zones":{"get":{"operationId":"list_cdn_zones","summary":"List CDN zones","description":"Standalone CDN zones (in front of any origin). Every app is already behind the CDN; these are for other origins.","tags":["cdn"],"security":[{"bearer":[]},{"oauth2":["cdn:read"]}],"x-cranl-scope":"cdn:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"zones":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["zones"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_cdn_zone","summary":"Create CDN zone","description":"Put the CDN in front of any origin URL (Pro and Enterprise). Point a hostname's CNAME at the returned cdn_target.","tags":["cdn"],"security":[{"bearer":[]},{"oauth2":["cdn:write"]}],"x-cranl-scope":"cdn:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":50},"origin_url":{"type":"string","format":"uri"},"edge_regions":{"description":"Default: all.","type":"array","items":{"type":"string","enum":["us","eu","asia","sa","af"]}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","origin_url"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cdn-zones/{zone_id}/purge":{"post":{"operationId":"purge_cdn_zone","summary":"Purge CDN zone","description":"Clear a CDN zone's cache: everything, or one URL.","tags":["cdn"],"security":[{"bearer":[]},{"oauth2":["cdn:write"]}],"x-cranl-scope":"cdn:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"zone_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"CDN zone id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes":{"get":{"operationId":"list_sandboxes","summary":"List sandboxes","description":"Cloud machines (sandboxes) in this account with their status, size, preview URLs and cost this month.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"status","in":"query","required":false,"schema":{"description":"Comma-separated statuses, e.g. running,sleeping","type":"string"},"description":"Comma-separated statuses, e.g. running,sleeping"},{"name":"label","in":"query","required":false,"schema":{"description":"Filter by label, key:value","type":"string"},"description":"Filter by label, key:value"},{"name":"q","in":"query","required":false,"schema":{"description":"Search by name or id","type":"string"},"description":"Search by name or id"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["data","next_cursor"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_sandbox","summary":"Create sandbox","description":"Create a cloud machine (Linux, root, internet access) and wait until it is running: seconds from a template, longer when cloning a repository. Pay as you go from prepaid credit. Pick one source: template (node, python, code-interpreter, ubuntu, go, rust, playwright…), repo (owner/name, cloned into /workspace) or image (any public Docker image). It deletes itself after ttl_minutes (default 60) without activity; pass keep: true to keep it. Then use sandbox_exec, sandbox_write_files and sandbox_run_code.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["billing:spend"]}],"x-cranl-scope":"billing:spend","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"template":{"description":"Template id, e.g. node, python, code-interpreter, ubuntu, playwright","type":"string"},"repo":{"description":"GitHub repository owner/name to clone (needs the account's GitHub connection)","type":"string"},"branch":{"type":"string"},"pull_request":{"description":"With repo: check out this pull request's head, to review or fix it","type":"integer","minimum":1,"maximum":9007199254740991},"image":{"description":"Any public Docker image instead of a template","type":"string"},"checkpoint_id":{"description":"Start from a checkpoint (a fork of another sandbox)","type":"string"},"name":{"type":"string","maxLength":60},"size":{"description":"small 1 vCPU/2 GB, medium 2/4 (default), large 4/8, xlarge 8/16","type":"string","enum":["small","medium","large","xlarge"]},"vcpu":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"ram_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"disk_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"ttl_minutes":{"description":"Delete it after this many minutes without activity (default 60). Each command, file write or preview visit resets the timer.","type":"integer","minimum":1,"maximum":1440},"keep":{"description":"Keep it until deleted (it sleeps when idle) instead of cleaning up after ttl_minutes.","type":"boolean"},"always_on":{"description":"Never sleep, never expire (billed around the clock)","type":"boolean"},"env":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"secrets":{"description":"Account secrets to set as environment variables for the sandbox's whole life, by name (list_sandbox_secrets; needs the env:reveal scope). The API never returns their values and masks them in output on a best-effort basis, but anything that runs commands in the sandbox can read them.","type":"array","items":{"type":"string"}},"install_command":{"description":"Run once after the source is ready, e.g. npm install","type":"string"},"run_command":{"description":"Kept running in the background as the process named app, e.g. a dev server","type":"string"},"ports":{"description":"Ports to give private preview URLs","type":"array","items":{"type":"integer","minimum":1,"maximum":65535}},"labels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"project_id":{"type":"string"},"wait":{"default":true,"description":"Wait until it is running (default). false returns right away with status creating.","type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}":{"get":{"operationId":"get_sandbox","summary":"Get sandbox","description":"One sandbox: status, size, lifecycle and when it expires, preview URLs, SSH command and cost this month.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_sandbox","summary":"Delete sandbox","description":"Delete the sandbox and its disk. Not recoverable (make a checkpoint first to keep its files). Destructive: needs a confirmation token.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"update_sandbox","summary":"Change sandbox settings","description":"Change a sandbox's name, labels, sleep and expiry timers, run command, internet access or daily cost alert. Only the fields you pass change. cost_alert: { daily_usd, action: notify | stop } emails the owner (and stops the sandbox with action stop) when it spends more than daily_usd in a day; null turns it off. A change that lets the sandbox run for longer (lifecycle always_on, or persistent from ephemeral; a later or null idle_timeout_seconds; a later expiry through ttl_seconds or ttl_mode) is spending: it needs the billing:spend scope and counts against an API key's monthly spend cap. allow_internet and egress cannot change while the sandbox is being created (conflict: try again once it runs), and an egress list is refused where the provider cannot enforce one (use allow_internet: false instead).","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"labels":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"lifecycle":{"type":"string","enum":["persistent","ephemeral","always_on"]},"idle_timeout_seconds":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"ttl_seconds":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"ttl_mode":{"type":"string","enum":["fixed","idle"]},"run_command":{"anyOf":[{"type":"string"},{"type":"null"}]},"install_command":{"anyOf":[{"type":"string"},{"type":"null"}]},"allow_internet":{"type":"boolean"},"egress":{"description":"Hosts the sandbox may reach when internet is limited","type":"array","items":{"type":"string"}},"cost_alert":{"anyOf":[{"type":"object","properties":{"daily_usd":{"type":"number","minimum":0.1,"maximum":10000},"action":{"default":"notify","type":"string","enum":["notify","stop"]}},"required":["daily_usd"]},{"type":"null"}]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/exec":{"post":{"operationId":"sandbox_exec","summary":"Run a command in a sandbox","description":"Run a shell command and get its exit code, stdout and stderr. Wakes the sandbox if it is asleep. Pass session (any name) to keep the working directory and exported variables between calls, like one terminal. For servers and watchers pass background: true, plus wait_for_port to get back a preview URL once it listens. Failures come with a hint (missing command, out of memory, timeout).","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"command":{"anyOf":[{"type":"string","minLength":1,"maxLength":131072},{"minItems":1,"type":"array","items":{"type":"string"}}],"description":"A string run with sh -c, or an argv array"},"cwd":{"description":"Absolute directory (default the sandbox's workdir, or where the session last was)","type":"string"},"env":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"timeout_seconds":{"default":300,"type":"integer","minimum":1,"maximum":3600},"session":{"description":"Shell session name: cd and export carry over between calls","type":"string","pattern":"^[A-Za-z0-9_-]{1,32}$"},"background":{"description":"Start it and return right away (servers, watchers, long jobs)","type":"boolean"},"name":{"description":"With background: a name for the process (lowercase, dashes)","type":"string"},"wait_for_port":{"description":"With background: wait until this port listens and return its preview URL","type":"integer","minimum":1,"maximum":65535},"public":{"description":"With wait_for_port: make the URL public (default private, with a 1-hour signed link)","type":"boolean"},"secrets":{"description":"Account secrets to put in this one command's environment, by name (needs the env:reveal scope). A session does not keep them, though it keeps what the command exports from them (export AUTH=\"Bearer $KEY\"), and a session variable of the same name is replaced for the command and then dropped; with background they stay in that process's environment. Their values are masked in output on a best-effort basis: a command that is given a secret can still read it, and so can anything else that runs in the sandbox, whatever its key's scopes.","type":"array","items":{"type":"string"}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["command"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/code":{"post":{"operationId":"sandbox_run_code","summary":"Run code in a sandbox (code interpreter)","description":"Run code in a stateful Jupyter kernel inside a sandbox. Variables, imports and files persist between calls, so build up work step by step. Returns stdout, stderr, the value of the last expression, errors with tracebacks, and charts or images (as images over MCP). Values of the sandbox's secrets are masked in the output on a best-effort basis; code running in the sandbox can still read them. Use a sandbox made from the code-interpreter template for data work: pandas, numpy, matplotlib, seaborn, scipy and scikit-learn are installed.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","maxLength":524288},"language":{"description":"Default python","type":"string","enum":["python","bash","javascript","typescript","r","java"]},"context_id":{"description":"A separate kernel with its own state","type":"string"},"timeout_seconds":{"default":300,"description":"Interrupts the code; state survives","type":"integer","minimum":1,"maximum":3600},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["code"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/files/batch":{"post":{"operationId":"sandbox_write_files","summary":"Write files in a sandbox","description":"Create or overwrite one or more files in one call (up to 200 files, 20 MB). Parent folders are created. Send binary content base64-encoded with encoding base64.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"files":{"minItems":1,"maxItems":200,"type":"array","items":{"type":"object","properties":{"path":{"type":"string","minLength":1,"description":"Absolute path, e.g. /workspace/app.py"},"content":{"type":"string"},"encoding":{"type":"string","enum":["utf8","base64"]}},"required":["path","content"]}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["files"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"written":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"size":{"type":"number"}},"required":["path","size"],"additionalProperties":false}}},"required":["written"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/files":{"get":{"operationId":"sandbox_read_file","summary":"Read a file in a sandbox","description":"Read a text file (up to 10 MB), or list a directory when the path is one. A wrong path answers with what the folder does contain.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"path","in":"query","required":true,"schema":{"type":"string","minLength":1,"description":"Absolute path"},"description":"Absolute path"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/files/tree":{"get":{"operationId":"sandbox_list_files","summary":"List files in a sandbox","description":"Every file and folder under a path as a tree (folders end in /), skipping node_modules, .git, virtualenvs and build output. Use it to get oriented in a project.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"path","in":"query","required":false,"schema":{"description":"Absolute directory (default the sandbox's workdir)","type":"string"},"description":"Absolute directory (default the sandbox's workdir)"},{"name":"depth","in":"query","required":false,"schema":{"default":3,"type":"integer","minimum":1,"maximum":10}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"path":{"type":"string"},"depth":{"type":"number"},"entries":{"type":"array","items":{"type":"string"}},"truncated":{"type":"boolean"}},"required":["path","depth","entries","truncated"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/http":{"post":{"operationId":"sandbox_http_request","summary":"Send an HTTP request to a sandbox app","description":"Call an app running in the sandbox (e.g. a dev server on port 3000) from outside, the way a browser or client would, and get the status, headers and body. Use it to test an API you just started. The port does not need to be public.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"port":{"type":"integer","minimum":1,"maximum":65535},"path":{"default":"/","description":"Path and query, e.g. /api/users?limit=5","type":"string"},"method":{"default":"GET","type":"string","enum":["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"]},"headers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"body":{"description":"Request body (send JSON as a string with a content-type header)","type":"string"},"timeout_seconds":{"default":30,"type":"integer","minimum":1,"maximum":120},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["port"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/screenshot":{"post":{"operationId":"sandbox_screenshot","summary":"Screenshot a page in a sandbox","description":"Take a screenshot of an app running in the sandbox (port, path) or any URL it can reach, to check what you built looks right. Returned as an image. The first capture installs a browser if the sandbox has none (about a minute); the playwright template has one ready.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"port":{"type":"integer","minimum":1,"maximum":65535},"path":{"description":"With port: the page, e.g. /dashboard","type":"string"},"url":{"description":"Instead of port: a full http(s) URL","type":"string"},"width":{"default":1280,"type":"integer","minimum":320,"maximum":1920},"height":{"default":800,"type":"integer","minimum":240,"maximum":4000},"wait_ms":{"default":2000,"description":"Time for the page to load and settle","type":"integer","minimum":0,"maximum":30000},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"width":{"type":"number"},"height":{"type":"number"},"png":{"type":"string"}},"required":["url","width","height","png"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/ports":{"post":{"operationId":"sandbox_expose_port","summary":"Expose a sandbox port","description":"Give a port inside the sandbox an HTTPS preview URL. Private URLs need a CranL sign-in, so the answer also has a signed_url anyone can open for an hour; public ones are open to anyone with the link. Opening a sleeping sandbox through the signed_url wakes it only if the key that made the link has billing:spend and room under its spend caps, and the wake counts against them; otherwise the page says the link cannot wake it.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"idempotent":true,"openWorld":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"port":{"type":"integer","minimum":1,"maximum":65535},"public":{"default":false,"type":"boolean"},"label":{"type":"string","maxLength":40},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["port"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/{action}":{"post":{"operationId":"sandbox_lifecycle","summary":"Start, stop, pause or restart a sandbox","description":"start or resume a sandbox, stop it (disk kept, compute billing stops), pause it (memory kept), restart it, retry a failed create, or keepalive to push back its expiry. Waits for the change to finish unless wait is false. Anything that starts a machine or keeps it up for longer spends credit and needs the billing:spend scope as well: start, resume, retry and keepalive always, restart only when the sandbox is sleeping. Those count against an API key's monthly spend cap, once per sandbox per month (stopping and starting again adds nothing).","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"action","in":"path","required":true,"schema":{"type":"string","enum":["start","resume","stop","pause","restart","retry","keepalive"]}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"wait":{"default":true,"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandbox-runs":{"post":{"operationId":"sandbox_run","summary":"Run something in a fresh sandbox","description":"One call for a one-off job: create a sandbox, write the files, run a command (or code), return the output, and delete the sandbox. Pass keep: true to keep it afterwards (its id is in the answer) and continue with sandbox_exec.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["billing:spend"]}],"x-cranl-scope":"billing:spend","x-cranl-annotations":{"openWorld":true},"parameters":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"template":{"description":"Default ubuntu, or code-interpreter when code is given","type":"string"},"image":{"type":"string"},"repo":{"type":"string"},"size":{"description":"small 1 vCPU/2 GB, medium 2/4 (default), large 4/8, xlarge 8/16","type":"string","enum":["small","medium","large","xlarge"]},"vcpu":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"ram_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"disk_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"files":{"maxItems":200,"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"},"encoding":{"type":"string","enum":["utf8","base64"]}},"required":["path","content"]}},"command":{"description":"Shell command to run","type":"string"},"code":{"description":"Or: code for the kernel (python by default)","type":"string"},"language":{"type":"string","enum":["python","bash","javascript","typescript","r","java"]},"env":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"timeout_seconds":{"default":600,"type":"integer","minimum":1,"maximum":3600},"keep":{"description":"Keep the sandbox afterwards (it still expires after 60 idle minutes)","type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"sandbox_id":{"type":"string"},"kept":{"type":"boolean"},"result":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["sandbox_id","kept","result"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/templates":{"get":{"operationId":"list_sandbox_templates","summary":"List sandbox templates","description":"Templates to create sandboxes from (ubuntu, node, python, code-interpreter, go, rust, jupyter, playwright…) with what each includes, the account's own checkpoint templates, and the prices.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/logs":{"get":{"operationId":"sandbox_logs","summary":"Sandbox process logs","description":"Output of a background process in the sandbox, and whether it is still running (the run_command is called app).","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"process","in":"query","required":false,"schema":{"default":"app","type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/processes":{"get":{"operationId":"list_sandbox_processes","summary":"List sandbox processes","description":"Background processes in the sandbox (servers, watchers, background runs) with their state and exit code.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"processes":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["processes"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/processes/{name}":{"post":{"operationId":"sandbox_process","summary":"Stop, start, restart or remove a sandbox process","description":"Control a background process by name.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"name","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["stop","start","restart","remove"]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["action"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/search":{"get":{"operationId":"sandbox_search","summary":"Search files in a sandbox","description":"Search file contents under a folder (like grep -rn), skipping node_modules and .git. Returns file, line and text for each match.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":500,"description":"Text to find"},"description":"Text to find"},{"name":"path","in":"query","required":false,"schema":{"description":"Absolute folder (default the workdir)","type":"string"},"description":"Absolute folder (default the workdir)"},{"name":"limit","in":"query","required":false,"schema":{"default":100,"type":"integer","minimum":1,"maximum":1000}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/resize":{"post":{"operationId":"sandbox_resize","summary":"Resize a sandbox","description":"Change CPU, memory or disk (disk can only grow). The sandbox restarts; files are kept. Use when a process is killed for memory (exit 137).","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["billing:spend"]}],"x-cranl-scope":"billing:spend","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"vcpu":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"ram_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"disk_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"wait":{"default":true,"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/checkpoints":{"post":{"operationId":"sandbox_checkpoint","summary":"Checkpoint a sandbox","description":"Save the sandbox's files as a checkpoint: roll back to it later with sandbox_restore, or fork it with create_sandbox { checkpoint_id }. Take one before a risky change. Waits until it is saved.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":80},"paths":{"description":"Only these absolute paths (default the workdir)","type":"array","items":{"type":"string"}},"exclude":{"description":"Names to leave out, e.g. node_modules","type":"array","items":{"type":"string"}},"template":{"description":"Also offer it as a template for new sandboxes","type":"boolean"},"wait":{"default":true,"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/restore":{"post":{"operationId":"sandbox_restore","summary":"Restore a sandbox from a checkpoint","description":"Replace the sandbox's files with a checkpoint's. Changes since then are lost. Destructive: needs a confirmation token.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"checkpoint_id":{"type":"string","minLength":1},"wait":{"default":true,"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["checkpoint_id"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/pull-request":{"post":{"operationId":"sandbox_create_pull_request","summary":"Open a pull request from a sandbox","description":"Commit every change in a sandbox created from a repository as one commit on a new branch and open a pull request, using the account's GitHub connection. The commit is made on CranL's side through GitHub's API: the sandbox never holds a GitHub credential, and its own git state is left as it is. branch must be new, or one this tool created for this sandbox before that nobody else has pushed to (it is then moved forward, never overwritten); the base branch and the repository's default branch are refused. An open pull request for the branch is reused, and no new commit is made when nothing changed since the last call and the base branch has not moved. Not available for sandboxes created from a fork's pull request. Limits: 500 changed files (at most 60 of them not plain text, such as images, fonts or archives), 5 MB per file, 25 MB in total. Returns the pull request URL.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":250},"body":{"description":"Pull request description (Markdown)","type":"string","maxLength":60000},"branch":{"description":"Default cranl/<title>-<random>. Must not exist yet, unless this tool created it for this sandbox.","type":"string"},"base":{"description":"Default the branch the sandbox was cloned from","type":"string"},"commit_message":{"type":"string"},"draft":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["title"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"number"},"url":{"type":"string"},"branch":{"type":"string"},"base":{"type":"string"},"commit":{"type":"string"},"files_changed":{"type":"number"}},"required":["number","url","branch","base","commit","files_changed"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/usage":{"get":{"operationId":"get_sandbox_usage","summary":"Sandbox credit and spend","description":"Credit balance, what is running and costing per hour right now, how many hours the credit lasts at that rate, the monthly budget, and this month's spend by sandbox. Check it before starting large or long-running sandboxes.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"month","in":"query","required":false,"schema":{"description":"YYYY-MM (default this month)","type":"string","pattern":"^\\d{4}-\\d{2}$"},"description":"YYYY-MM (default this month)"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/diff":{"get":{"operationId":"sandbox_diff","summary":"What changed in a sandbox","description":"Every file added, modified or deleted in the working folder since the sandbox became ready (since: baseline) or since a checkpoint, with a unified diff. Works for any sandbox, repository or not. Use it to review your work before reporting or opening a pull request.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"since","in":"query","required":false,"schema":{"default":"baseline","description":"\"baseline\" (when it became ready) or a checkpoint id","type":"string"},"description":"\"baseline\" (when it became ready) or a checkpoint id"},{"name":"path","in":"query","required":false,"schema":{"description":"Only this file or folder, relative to the working folder","type":"string"},"description":"Only this file or folder, relative to the working folder"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"since":{"type":"string"},"files":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"diff":{"type":"string"},"truncated":{"type":"boolean"}},"required":["since","files","diff","truncated"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/changes":{"get":{"operationId":"sandbox_file_changes","summary":"Files changed since you last looked","description":"Files created, modified or deleted in a folder since your previous call. Call once without cursor to start, then pass the returned cursor each time (for example after starting a build or a dev server, to see what it wrote). Cursors last an hour.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"path","in":"query","required":false,"schema":{"description":"Absolute folder (default the working folder)","type":"string"},"description":"Absolute folder (default the working folder)"},{"name":"cursor","in":"query","required":false,"schema":{"description":"From your previous call","type":"string"},"description":"From your previous call"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"cursor":{"type":"string"},"changes":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"first":{"type":"boolean"}},"required":["cursor","changes","first"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/git":{"post":{"operationId":"sandbox_git","summary":"Git in a sandbox","description":"Git in the sandbox's repository: status (branch, ahead/behind, changed files), diff (optionally staged or one path), log, commit (stages everything, commits as the account's user), branch (create and switch) or checkout. To publish, use sandbox_create_pull_request.","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["status","diff","log","commit","branch","checkout"]},"path":{"description":"diff: only this path","type":"string"},"staged":{"description":"diff: staged changes","type":"boolean"},"message":{"description":"commit: the message","type":"string","maxLength":5000},"name":{"description":"branch/checkout: the branch","type":"string"},"limit":{"description":"log: how many commits","type":"integer","minimum":1,"maximum":200},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["action"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/browser":{"post":{"operationId":"sandbox_browser","summary":"Use a browser in a sandbox","description":"Drive a real browser inside the sandbox to test a web app end to end: go to a URL, click (CSS selector or text=Label), type, press keys, wait, scroll, run JavaScript. The browser stays open between calls (logins and cookies persist; new_session starts clean). Each call returns the page URL and title, each action's result, console errors, and a screenshot as an image. Apps in the sandbox are at http://localhost:PORT. Needs a browser and Node.js (the playwright template has both).","tags":["sandboxes"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"actions":{"minItems":1,"maxItems":30,"type":"array","items":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"goto"},"url":{"type":"string","description":"e.g. http://localhost:3000/login"}},"required":["type","url"]},{"type":"object","properties":{"type":{"type":"string","const":"click"},"selector":{"type":"string","description":"CSS selector, or text=Label to match visible text"}},"required":["type","selector"]},{"type":"object","properties":{"type":{"type":"string","const":"type"},"text":{"type":"string"},"selector":{"description":"Field to click first","type":"string"},"submit":{"description":"Press Enter after","type":"boolean"}},"required":["type","text"]},{"type":"object","properties":{"type":{"type":"string","const":"press"},"key":{"type":"string","description":"Enter, Tab, Escape, ArrowDown… or one character"}},"required":["type","key"]},{"type":"object","properties":{"type":{"type":"string","const":"wait"},"ms":{"type":"integer","minimum":-9007199254740991,"maximum":30000},"selector":{"type":"string"},"timeout_ms":{"type":"integer","minimum":-9007199254740991,"maximum":30000}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","const":"scroll"},"y":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["type","y"]},{"type":"object","properties":{"type":{"type":"string","const":"eval"},"expression":{"type":"string","description":"JavaScript run in the page; its value is returned"}},"required":["type","expression"]}]}},"width":{"type":"integer","minimum":320,"maximum":1920},"height":{"type":"integer","minimum":240,"maximum":2000},"new_session":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["actions"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"title":{"type":"string"},"results":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"png":{"type":"string"}},"required":["url","title","results","png"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/save-point":{"post":{"operationId":"sandbox_save_machine","summary":"Save a running sandbox","description":"Save the whole running machine (memory, running processes and files) as its save point, replacing the previous one. Roll back with sandbox_restore_machine. Faster than a checkpoint and keeps processes, but there is one save point per sandbox and it cannot be copied to another sandbox (use sandbox_fork for that).","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/save-point/restore":{"post":{"operationId":"sandbox_restore_machine","summary":"Roll a sandbox back to its save point","description":"Return the machine to its last save (sandbox_save_machine): memory, processes and files as they were. Everything since is lost. Destructive: needs a confirmation token. The machine is left running, so restoring one that is not running starts it: that needs the billing:spend scope, an account that may run sandboxes, a free running slot, and counts against an API key's monthly spend cap.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:write"]}],"x-cranl-scope":"sandboxes:write","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/{sandbox_id}/fork":{"post":{"operationId":"sandbox_fork","summary":"Fork a sandbox","description":"Make 1-10 copies of a running sandbox, each with its working folder, size, lifecycle and environment, to try different approaches side by side. Processes start fresh in each copy. Waits until they are running unless wait is false. Each copy is billed like any sandbox.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["billing:spend"]}],"x-cranl-scope":"billing:spend","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Sandbox id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"count":{"default":1,"type":"integer","minimum":1,"maximum":10},"name":{"type":"string","maxLength":50},"wait":{"default":true,"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"source":{"type":"string"},"sandboxes":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["source","sandboxes"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandbox-runs/parallel":{"post":{"operationId":"sandbox_parallel_run","summary":"Run variants in parallel sandboxes","description":"Run up to 10 variants side by side, each in its own fresh sandbox (or a fork of from_sandbox), and compare the results: exit code, time and output for each. Variants can differ by command, code or environment, e.g. two implementations or three library versions. Sandboxes are deleted afterwards unless keep is true.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["billing:spend"]}],"x-cranl-scope":"billing:spend","x-cranl-annotations":{"openWorld":true},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"variants":{"minItems":1,"maxItems":10,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","maxLength":40},"command":{"type":"string"},"code":{"type":"string"},"env":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}}}},"from_sandbox":{"description":"Fork this running sandbox for each variant instead of starting fresh","type":"string"},"template":{"type":"string"},"image":{"type":"string"},"repo":{"type":"string"},"size":{"description":"small 1 vCPU/2 GB, medium 2/4 (default), large 4/8, xlarge 8/16","type":"string","enum":["small","medium","large","xlarge"]},"vcpu":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"ram_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"disk_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"files":{"description":"Written into every sandbox first","maxItems":200,"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"content":{"type":"string"},"encoding":{"type":"string","enum":["utf8","base64"]}},"required":["path","content"]}},"language":{"type":"string","enum":["python","bash","javascript","typescript","r","java"]},"timeout_seconds":{"default":600,"type":"integer","minimum":1,"maximum":3600},"keep":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["variants"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"results":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"summary":{"type":"string"}},"required":["results","summary"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandbox-secrets":{"get":{"operationId":"list_sandbox_secrets","summary":"List sandbox secrets","description":"Names of the account's secrets (API keys and tokens a person saved). Use them by name: create_sandbox { secrets } or sandbox_exec { secrets }. The API never returns their values, and what it reads back from a sandbox (command and code output, logs and the build log, the timeline, file reads, search results, diffs, sandbox_http_request responses and browser console messages) shows [secret hidden] in place of a value or a common encoding of it (base64, hex, URL-encoded, JSON- or shell-escaped); screenshots, the terminal and SSH are not masked. File archives (files?archive=1) come as they are, nothing masked, and need sandboxes:write. That masking is best effort, not a guarantee: a key that can run commands with a secret can extract it, so only let a key use secrets it is trusted to know. A secret is only as private as the sandbox it is used in: anything else that runs there, including a key with sandboxes:write but not env:reveal, can capture it while a command uses it. Masking knows only the values the account holds now: once a secret is changed or deleted, its old value, and anything a session exported from it or a process wrote out with it, may no longer be masked wherever it is still found, in the sandbox (session state, logs, files, and so diffs) or in the build log and the timeline, so rotate a leaked or retired secret at its provider as well. Per-call env names must be plain shell names (letters, digits and _, not starting with a digit).","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"secrets":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"updated_at":{"type":"string"}},"required":["name","updated_at"],"additionalProperties":false}}},"required":["secrets"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/templates/builds":{"post":{"operationId":"build_sandbox_template","summary":"Build a sandbox template from a Dockerfile","description":"Turn a Dockerfile into a template so new sandboxes start with everything installed instead of repeating the setup. The steps run once in a builder sandbox (billed while it runs); pass the files COPY reads in files. Waits until done unless wait is false, then create sandboxes with create_sandbox { template: <the returned id> }. Supports FROM (one stage), RUN, COPY, ADD (no URLs), ENV, ARG, WORKDIR, EXPOSE, CMD, ENTRYPOINT.","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["billing:spend"]}],"x-cranl-scope":"billing:spend","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":60},"dockerfile":{"type":"string","minLength":1,"maxLength":100000},"files":{"description":"Build context: { \"relative/path\": \"content\" }, \"base64:…\" for binary","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"wait":{"default":true,"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","dockerfile"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/sandboxes/templates/builds/{build_id}":{"get":{"operationId":"get_sandbox_template_build","summary":"Get a template build","description":"Status and log of a template build (build_sandbox_template).","tags":["sandbox_extras"],"security":[{"bearer":[]},{"oauth2":["sandboxes:read"]}],"x-cranl-scope":"sandboxes:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"build_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/agents":{"get":{"operationId":"list_cloud_agents","summary":"List coding agents","description":"The coding agents Cloud Agents can run (Claude Code, Codex, OpenCode, Gemini CLI, Qwen Code, Cursor CLI, Amp, Aider, Goose, or a custom command), their suggested models, and whether this account has a key for each. Model keys stay on CranL's servers; the agent's machine only gets a gateway token.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/runs":{"post":{"operationId":"create_agent_run","summary":"Start a coding agent","description":"Start a coding agent on a task. It gets its own cloud machine with the repository checked out on a new branch, works until it is done, and (with repo) opens a draft pull request. Pass agents: [...] (or count) to have several agents try the same task in parallel and compare them. Returns at once; follow with wait_agent_run or get_agent_run_events. Billed per second of machine time; model calls go to the account's own keys.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prompt":{"type":"string","minLength":1,"maxLength":50000,"description":"The task, as you would tell an engineer"},"title":{"type":"string","maxLength":200},"agent":{"description":"Default: claude-code, or the environment's default","type":"string","enum":["claude-code","codex","opencode","gemini","qwen","cursor","amp","aider","goose","custom"]},"model":{"type":"string","maxLength":120},"agents":{"description":"Several agents on the same task, compared side by side","minItems":1,"maxItems":6,"type":"array","items":{"type":"object","properties":{"agent":{"type":"string","enum":["claude-code","codex","opencode","gemini","qwen","cursor","amp","aider","goose","custom"]},"model":{"description":"Model id; the agent's default when left out","type":"string","maxLength":120},"command":{"description":"For the custom agent: the command to run","type":"string","maxLength":4000}},"required":["agent"]}},"count":{"description":"Run the same agent this many times","type":"integer","minimum":1,"maximum":6},"repo":{"description":"GitHub repository, owner/name","type":"string","maxLength":255},"branch":{"description":"Branch to start from and open the pull request against. Default: the repository's default branch","type":"string","maxLength":255},"environment_id":{"description":"An environment (env_…): setup script, variables, secrets, network rules and defaults","type":"string"},"command":{"description":"For the custom agent: the command to run; the task is in $CRANL_PROMPT","type":"string","maxLength":4000},"size":{"type":"string","enum":["small","medium","large","xlarge"]},"instructions":{"description":"Standing instructions added to the agent's system prompt","type":"string","maxLength":8000},"max_minutes":{"description":"Stop the run after this long","type":"integer","minimum":1,"maximum":1440},"max_cost_usd":{"description":"Stop the run when its model calls cost this much","type":"number","minimum":0.01,"maximum":10000},"auto_pr":{"description":"Open a pull request when the agent finishes (default true with repo)","type":"boolean"},"draft_pr":{"description":"Open the pull request as a draft (default true)","type":"boolean"},"keep_sandbox":{"description":"Keep the machine after the run instead of deleting it after a few idle hours","type":"boolean"},"project_id":{"type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["prompt"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"list_agent_runs","summary":"List agent runs","description":"Recent Cloud Agents runs, newest first. Filter by status (or \"active\"), repo, agent, group or automation.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["active","queued","starting","running","completed","failed","stopped"]}},{"name":"repo","in":"query","required":false,"schema":{"type":"string"}},{"name":"agent","in":"query","required":false,"schema":{"type":"string","enum":["claude-code","codex","opencode","gemini","qwen","cursor","amp","aider","goose","custom"]}},{"name":"group_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"automation_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"include_archived","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}},{"name":"before","in":"query","required":false,"schema":{"description":"A run id: list runs older than it","type":"string"},"description":"A run id: list runs older than it"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/runs/{run_id}":{"get":{"operationId":"get_agent_run","summary":"Get an agent run","description":"One run: status, what phase it is in, the agent's summary, tokens and cost, the diff size and the pull request.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Run id (run_…)"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"archive_agent_run","summary":"Archive an agent run","description":"Take a run off the board and delete its machine. The timeline and any pull request stay.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Run id (run_…)"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/runs/{run_id}/wait":{"post":{"operationId":"wait_agent_run","summary":"Wait for an agent run","description":"Wait until the run finishes its current work (completed, failed or stopped), up to timeout_seconds (default 300, at most 840). Returns the run; if it is still working, call again.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Run id (run_…)"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"timeout_seconds":{"type":"integer","minimum":1,"maximum":840}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/runs/{run_id}/events":{"get":{"operationId":"get_agent_run_events","summary":"Read an agent's timeline","description":"What the agent did, in order: messages, thinking, tool calls and their results, commands, file edits, plans, errors. Pass after (the last seq you saw) to get only what is new.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Run id (run_…)"}},{"name":"after","in":"query","required":false,"schema":{"type":"integer","minimum":0,"maximum":9007199254740991}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":2000}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/runs/{run_id}/messages":{"post":{"operationId":"send_agent_message","summary":"Message an agent","description":"Send the agent a follow-up. While it works, the message waits for its current turn to end; once it is done, the message starts a new turn in the same machine (and the same conversation, for agents that support it). The pull request is updated when the turn finishes.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{},"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Run id (run_…)"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","minLength":1,"maxLength":20000},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["message"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/runs/{run_id}/stop":{"post":{"operationId":"stop_agent_run","summary":"Stop an agent run","description":"Stop the agent now. Its work so far stays in the machine: review the diff, open the pull request, or send a message to carry on.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Run id (run_…)"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":300},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/runs/{run_id}/diff":{"get":{"operationId":"get_agent_run_diff","summary":"Review an agent's changes","description":"The run's changes against the commit it started from: a unified diff (up to 1 MB) and per-file line counts.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Run id (run_…)"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/runs/{run_id}/pull-request":{"post":{"operationId":"create_agent_run_pr","summary":"Open the run's pull request","description":"Commit the run's changes to its branch and open the pull request, or update it if it is open already. Done by CranL's server; the machine never holds a GitHub token.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"run_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Run id (run_…)"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","maxLength":250},"body":{"type":"string","maxLength":60000},"draft":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/groups/{group_id}":{"get":{"operationId":"get_agent_group","summary":"Compare parallel runs","description":"Runs that worked on the same task side by side: status, diff size, tokens, cost, time and summary for each, and which one was picked.","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/groups/{group_id}/winner":{"post":{"operationId":"pick_agent_group_winner","summary":"Pick the best run","description":"Keep one run of a comparison: open its pull request, and archive the others (deleting their machines).","tags":["cloud_agents"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"run_id":{"type":"string","minLength":1,"description":"Run id (run_…)"},"open_pr":{"type":"boolean"},"archive_others":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["run_id"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/keys":{"get":{"operationId":"list_agent_model_keys","summary":"List model keys","description":"The model provider keys saved for Cloud Agents (last four characters only; the keys are never returned).","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/keys/{provider}":{"put":{"operationId":"set_agent_model_key","summary":"Save a model key","description":"Save (or replace) the account's key for a model provider. Anthropic, OpenAI, Google and OpenRouter keys never leave CranL's servers: agents reach the model through the gateway with a short-lived run token. Cursor and Amp keys are handed to the machine, because those agents call their vendor directly.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["anthropic","openai","google","openrouter","cursor","amp"]}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string","minLength":8,"maxLength":500,"description":"The API key"},"label":{"type":"string","maxLength":120},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["key"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_agent_model_key","summary":"Remove a model key","description":"Remove the account's key for a model provider. Runs in progress that use it fail on their next model call.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","enum":["anthropic","openai","google","openrouter","cursor","amp"]}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/environments":{"get":{"operationId":"list_agent_environments","summary":"List agent environments","description":"Environments: how a repository is set up for agents (setup script, variables, secrets, network rules, defaults) and whether a warm snapshot is ready.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_agent_environment","summary":"Create an agent environment","description":"Describe how a repository is set up for agents once, and start runs from it with environment_id.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"repo":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"branch":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"setup_script":{"description":"Runs in the repository before the agent starts, e.g. npm ci","anyOf":[{"type":"string","maxLength":20000},{"type":"null"}]},"env":{"description":"Variables for the machine; null removes one","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]}},"secrets":{"description":"Account secret names to put in the machine's environment (needs env:reveal)","maxItems":50,"type":"array","items":{"type":"string"}},"egress_mode":{"description":"open: the whole internet; restricted: package registries, GitHub and the egress list; none: only the model gateway","type":"string","enum":["open","restricted","none"]},"egress":{"maxItems":100,"type":"array","items":{"type":"string"}},"size":{"type":"string","enum":["small","medium","large","xlarge"]},"default_agent":{"anyOf":[{"type":"string","enum":["claude-code","codex","opencode","gemini","qwen","cursor","amp","aider","goose","custom"]},{"type":"null"}]},"default_model":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"instructions":{"anyOf":[{"type":"string","maxLength":8000},{"type":"null"}]},"auto_pr":{"type":"boolean"},"max_minutes":{"anyOf":[{"type":"integer","minimum":1,"maximum":1440},{"type":"null"}]},"max_cost_usd":{"anyOf":[{"type":"number","minimum":0.01,"maximum":10000},{"type":"null"}]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/environments/{environment_id}":{"patch":{"operationId":"update_agent_environment","summary":"Update an agent environment","description":"Change an environment. A new repository, branch or setup script marks the warm snapshot out of date.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"environment_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"repo":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"branch":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"setup_script":{"description":"Runs in the repository before the agent starts, e.g. npm ci","anyOf":[{"type":"string","maxLength":20000},{"type":"null"}]},"env":{"description":"Variables for the machine; null removes one","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]}},"secrets":{"description":"Account secret names to put in the machine's environment (needs env:reveal)","maxItems":50,"type":"array","items":{"type":"string"}},"egress_mode":{"description":"open: the whole internet; restricted: package registries, GitHub and the egress list; none: only the model gateway","type":"string","enum":["open","restricted","none"]},"egress":{"maxItems":100,"type":"array","items":{"type":"string"}},"size":{"type":"string","enum":["small","medium","large","xlarge"]},"default_agent":{"anyOf":[{"type":"string","enum":["claude-code","codex","opencode","gemini","qwen","cursor","amp","aider","goose","custom"]},{"type":"null"}]},"default_model":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"instructions":{"anyOf":[{"type":"string","maxLength":8000},{"type":"null"}]},"auto_pr":{"type":"boolean"},"max_minutes":{"anyOf":[{"type":"integer","minimum":1,"maximum":1440},{"type":"null"}]},"max_cost_usd":{"anyOf":[{"type":"number","minimum":0.01,"maximum":10000},{"type":"null"}]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_agent_environment","summary":"Delete an agent environment","description":"Delete an environment and its snapshot. Runs already made from it are not affected.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"environment_id","in":"path","required":true,"schema":{"type":"string","minLength":1}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/environments/{environment_id}/snapshot":{"post":{"operationId":"build_agent_environment_snapshot","summary":"Warm up an environment","description":"Build the environment's warm snapshot: a machine runs the setup script and installs the default agent, and what it installed is kept. Runs then start with it in place. Takes a few minutes; the environment's snapshot.status says when it is ready.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{},"parameters":[{"name":"environment_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/automations":{"get":{"operationId":"list_agent_automations","summary":"List agent automations","description":"Automations that start runs on their own: GitHub labels and @mentions, schedules, signed webhooks (Sentry, Linear, anything) and Slack.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_agent_automation","summary":"Create an agent automation","description":"Start runs automatically. github: label an issue (default label cranl-agent) or mention @cranl in a comment; on a pull request a run opened, the comment becomes a follow-up. schedule: a cron expression. webhook: POST signed JSON to hook_url (the secret is returned once). slack: point your Slack app's slash command at hook_url.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"config":{"description":"schedule: { cron, timezone }; github: { label, mention, on_label, on_mention }; webhook: { source: \"generic\"|\"sentry\"|\"linear\", max_per_hour }; slack: { max_per_hour }","type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"repo":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"branch":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"environment_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"agent":{"type":"string","enum":["claude-code","codex","opencode","gemini","qwen","cursor","amp","aider","goose","custom"]},"model":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"prompt":{"description":"What to do each time. Placeholders: {{issue.title}}, {{issue.body}}, {{comment.body}}, {{title}}, {{payload}}, {{text}}, {{repo}}","type":"string","maxLength":20000},"enabled":{"type":"boolean"},"signing_secret":{"description":"slack: your Slack app's signing secret","type":"string"},"trigger":{"type":"string","enum":["github","schedule","webhook","slack"]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","trigger"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/automations/{automation_id}":{"patch":{"operationId":"update_agent_automation","summary":"Update an agent automation","description":"Change an automation, or pause it with enabled: false.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"automation_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":120},"config":{"description":"schedule: { cron, timezone }; github: { label, mention, on_label, on_mention }; webhook: { source: \"generic\"|\"sentry\"|\"linear\", max_per_hour }; slack: { max_per_hour }","type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"repo":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"branch":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}]},"environment_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"agent":{"type":"string","enum":["claude-code","codex","opencode","gemini","qwen","cursor","amp","aider","goose","custom"]},"model":{"anyOf":[{"type":"string","maxLength":120},{"type":"null"}]},"prompt":{"description":"What to do each time. Placeholders: {{issue.title}}, {{issue.body}}, {{comment.body}}, {{title}}, {{payload}}, {{text}}, {{repo}}","type":"string","maxLength":20000},"enabled":{"type":"boolean"},"signing_secret":{"description":"slack: your Slack app's signing secret","type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_agent_automation","summary":"Delete an agent automation","description":"Delete an automation. Runs it started are kept.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"automation_id","in":"path","required":true,"schema":{"type":"string","minLength":1}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/automations/{automation_id}/rotate-secret":{"post":{"operationId":"rotate_agent_automation_secret","summary":"Rotate a webhook secret","description":"Give a webhook automation a new signing secret (returned once). The old one stops working at once.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{},"parameters":[{"name":"automation_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/automations/{automation_id}/run":{"post":{"operationId":"run_agent_automation","summary":"Run an automation now","description":"Start an automation's run now, as if it had been triggered. vars fill its prompt's placeholders.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{},"parameters":[{"name":"automation_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"vars":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/workspaces":{"get":{"operationId":"list_agent_workspaces","summary":"List agent workspaces","description":"Persistent machines with coding agents installed and model keys wired through the gateway, to use by hand from the browser terminal, SSH or a desktop editor.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_agent_workspace","summary":"Create an agent workspace","description":"A persistent machine with the chosen agents installed and ready (type claude or codex in its terminal; keys come through the gateway). It sleeps when idle and keeps its files. Connect with SSH or the dashboard's terminal.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":63},"repo":{"type":"string","maxLength":255},"branch":{"type":"string","maxLength":255},"environment_id":{"type":"string"},"agents":{"maxItems":9,"type":"array","items":{"type":"string","enum":["claude-code","codex","opencode","gemini","qwen","cursor","amp","aider","goose","custom"]}},"size":{"type":"string","enum":["small","medium","large","xlarge"]},"project_id":{"type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/workspaces/{sandbox_id}":{"delete":{"operationId":"delete_agent_workspace","summary":"Delete an agent workspace","description":"Delete a workspace machine and its files, and revoke its gateway token.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"sandbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cloud-agents/settings":{"get":{"operationId":"get_cloud_agent_settings","summary":"Get Cloud Agents settings","description":"Notifications and limits for Cloud Agents in this account.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:read"]}],"x-cranl-scope":"agents:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"update_cloud_agent_settings","summary":"Update Cloud Agents settings","description":"Turn run notifications on or off (email to whoever started the run; the account's chat channels) and set how many runs may work at once.","tags":["cloud_agent_setup"],"security":[{"bearer":[]},{"oauth2":["agents:write"]}],"x-cranl-scope":"agents:write","x-cranl-annotations":{"idempotent":true},"parameters":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"notify_email":{"type":"boolean"},"notify_channels":{"type":"boolean"},"max_concurrent":{"type":"integer","minimum":1,"maximum":25},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime":{"get":{"operationId":"get_realtime_overview","summary":"Realtime overview","description":"Whether this account's plan includes Realtime (Pro and Enterprise do), its limits (apps, concurrent connections, messages a month, message size), this month's usage, every Realtime app with its live connection and channel counts, and the gateway URLs.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps":{"get":{"operationId":"list_realtime_apps","summary":"List Realtime apps","description":"The account's Realtime apps with their settings, publishable key (safe for browsers) and live connection and channel counts.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_realtime_app","summary":"Create Realtime app","description":"Create a Realtime app (Pro and Enterprise plans). Returns its id, publishable key and, once, its secret key (with the realtime:secrets scope; otherwise fetch it later with get_realtime_keys). Without allowed_origins any website may connect with the publishable key; private and presence channels always need a token signed with the secret key. Next: connect_realtime_to_app to put the keys in an app's environment.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"project_id":{"description":"Defaults to the account's first project","type":"string","minLength":1},"allowed_origins":{"description":"Browser origins allowed to connect, e.g. [\"https://app.example.com\", \"http://localhost:*\"]","maxItems":20,"type":"array","items":{"type":"string"}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}":{"get":{"operationId":"get_realtime_app","summary":"Get Realtime app","description":"One Realtime app: settings, publishable key, the secret key's first characters, live counts.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"update_realtime_app","summary":"Update Realtime app","description":"Change a Realtime app's name, whether it is enabled (disabling ends every connection), allowed origins, history (messages kept per channel and for how long; 0 turns it off), client events (clients publishing directly, still limited by their token), the largest message (up to the plan's limit) and the fields masked in the inspector. Live connections pick changes up at once.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"enabled":{"type":"boolean"},"allowed_origins":{"maxItems":20,"type":"array","items":{"type":"string"}},"allow_any_origin":{"type":"boolean"},"history_messages":{"type":"integer","minimum":0,"maximum":1000},"history_seconds":{"type":"integer","minimum":0,"maximum":86400},"client_events":{"type":"boolean"},"message_kb":{"type":"integer","minimum":1,"maximum":256},"masked_fields":{"maxItems":50,"type":"array","items":{"type":"string"}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_realtime_app","summary":"Delete Realtime app","description":"Delete a Realtime app. Its keys stop working and every connection ends at once. This cannot be undone.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:delete"]}],"x-cranl-scope":"realtime:delete","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/keys":{"get":{"operationId":"get_realtime_keys","summary":"Get Realtime keys","description":"The app's keys. The publishable key (rt_pk_) is always shown: it is safe in browsers and only allows public channels on its own. The secret key (rt_sk_) is shown with reveal: true, which needs the realtime:secrets scope. A rotated key shows as retiring until its grace period ends.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"reveal","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/keys/rotate":{"post":{"operationId":"rotate_realtime_key","summary":"Rotate Realtime key","description":"Issue a new publishable or secret key. The old one keeps working for grace_minutes (default 1440, a day) so servers can move to the new one first; grace_minutes: 0 ends it now, along with every connection it authorized. Returns the new key once.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:secrets"]}],"x-cranl-scope":"realtime:secrets","x-cranl-annotations":{},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["publishable","secret"]},"grace_minutes":{"type":"integer","minimum":0,"maximum":10080},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["kind"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/connect":{"post":{"operationId":"connect_realtime_to_app","summary":"Connect Realtime to an app","description":"Put a Realtime app's URL and keys into an application's environment and restart it: CRANL_REALTIME_URL, CRANL_REALTIME_APP_ID and CRANL_REALTIME_SECRET_KEY for server code, plus the URL, app id and publishable key again under public_prefix (default NEXT_PUBLIC_; VITE_ or PUBLIC_ for other frameworks, or empty) for browser code. The secret key is never given the public prefix.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:secrets"]}],"x-cranl-scope":"realtime:secrets","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"app_id":{"type":"string","minLength":1,"description":"The CranL application to configure"},"public_prefix":{"type":"string","enum":["NEXT_PUBLIC_","VITE_","PUBLIC_","EXPO_PUBLIC_","REACT_APP_",""]},"restart":{"description":"Default true.","type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["app_id"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"array","items":{"type":"string"}},"app_id":{"type":"string"},"restarted":{"type":"boolean"},"env_version":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["variables","app_id","restarted","env_version"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/messages":{"post":{"operationId":"publish_realtime_message","summary":"Publish a Realtime message","description":"Send a message to everyone subscribed to a channel, on every gateway node, in order. data is any JSON up to the app's message size. It is kept in the channel's history when history is on, so clients that reconnect catch up.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"channel":{"type":"string","minLength":1,"maxLength":164,"description":"Channel name, e.g. \"public:news\", \"private:user.42\" or \"presence:room.1\""},"event":{"type":"string","minLength":1,"maxLength":100,"description":"Event name, e.g. order.updated"},"data":{"description":"Any JSON value"}},"required":["channel","event","data"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/tokens":{"post":{"operationId":"create_realtime_token","summary":"Create a Realtime client token","description":"Sign a client token for one user, for testing or for servers that cannot use the SDK (the SDK signs locally with the secret key). capabilities say what the user may do, e.g. [{ \"channel\": \"private:user.{sub}\", \"actions\": [\"subscribe\"] }, { \"channel\": \"presence:room.*\", \"actions\": [\"subscribe\", \"presence\"] }]. info is shown to other members of presence channels. Tokens live ttl_seconds (default an hour, at most a day).","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"user":{"description":"The user's id in your app (letters, digits and _ - = @ ,)","type":"string","maxLength":128},"capabilities":{"minItems":1,"maxItems":50,"type":"array","items":{"type":"object","properties":{"channel":{"type":"string","description":"Channel pattern: \"*\" matches one segment, a trailing \".**\" one or more, \"{sub}\" is the token's own user"},"actions":{"minItems":1,"type":"array","items":{"type":"string","enum":["subscribe","publish","presence"]}}},"required":["channel","actions"]}},"info":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"ttl_seconds":{"type":"integer","minimum":30,"maximum":86400}},"required":["capabilities"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/channels":{"get":{"operationId":"list_realtime_channels","summary":"List live Realtime channels","description":"Channels with at least one subscriber right now, busiest first, with their subscriber counts.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"prefix","in":"query","required":false,"schema":{"description":"Only channels starting with this, e.g. \"presence:\"","type":"string","maxLength":164},"description":"Only channels starting with this, e.g. \"presence:\""}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/channels/{channel}":{"get":{"operationId":"get_realtime_channel","summary":"Get a Realtime channel","description":"One channel: subscribers now, presence members (presence: channels) and its most recent messages from history. Fields the app masks (and passwords, tokens and the like) are hidden.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"channel","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":164,"description":"Channel name, e.g. \"public:news\", \"private:user.42\" or \"presence:room.1\""}},{"name":"history_limit","in":"query","required":false,"schema":{"type":"integer","minimum":0,"maximum":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/channels/{channel}/wait":{"post":{"operationId":"wait_realtime_message","summary":"Wait for a Realtime message","description":"Wait up to timeout_seconds (default 30, at most 60) for the next message on a channel, optionally of one event. Use it to check that code you wrote publishes what it should: start waiting, trigger the action, read the message.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"channel","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":164,"description":"Channel name, e.g. \"public:news\", \"private:user.42\" or \"presence:room.1\""}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"event":{"type":"string","maxLength":100},"timeout_seconds":{"type":"integer","minimum":1,"maximum":60}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/kick":{"post":{"operationId":"kick_realtime_user","summary":"End a user's Realtime connections","description":"End every connection of a user (or of one token, by token_id) on every gateway node. With revoke: true their existing tokens stay refused until they expire, so they cannot simply reconnect.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"string","maxLength":128},"token_id":{"type":"string","maxLength":64},"revoke":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/stats":{"get":{"operationId":"get_realtime_stats","summary":"Realtime usage","description":"Connections and channels now, this month's messages, the last hour by minute (messages in and out, peak connections) and the last 30 days by day.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/inspector":{"post":{"operationId":"open_realtime_inspector","summary":"Open the Realtime inspector","description":"A read-only connection for watching every message of the app live (subscribe to the channel \"*\"). The token lasts 15 minutes; the gateway masks sensitive fields before anything reaches it.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/webhooks":{"get":{"operationId":"list_realtime_webhooks","summary":"List Realtime webhooks","description":"Webhooks of a Realtime app with their last delivery status.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_realtime_webhook","summary":"Create Realtime webhook","description":"Be told when a channel gets its first subscriber (channel.occupied) or loses its last (channel.vacated), when a member joins or leaves a presence channel (member.added, member.removed) and when a client publishes (client.event). Deliveries are signed (CranL-Signature) and retried with backoff. Returns the signing secret once.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"A public https URL"},"events":{"minItems":1,"type":"array","items":{"type":"string","enum":["channel.occupied","channel.vacated","member.added","member.removed","client.event"]}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["url","events"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/webhooks/{webhook_id}":{"delete":{"operationId":"delete_realtime_webhook","summary":"Delete Realtime webhook","description":"Stop sending a webhook.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Webhook id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/webhooks/{webhook_id}/deliveries":{"get":{"operationId":"list_realtime_webhook_deliveries","summary":"Realtime webhook deliveries","description":"The last 50 delivery attempts of a Realtime webhook (kept a week): event, attempt, status the endpoint answered, time taken and the error, if any.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Webhook id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/webhooks/{webhook_id}/test":{"post":{"operationId":"test_realtime_webhook","summary":"Send a test webhook","description":"Send a signed \"webhook.test\" event to a Realtime webhook now and report what the endpoint answered.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Webhook id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/users":{"get":{"operationId":"list_realtime_users","summary":"List connected users","description":"Users connected to a Realtime app right now (by the user id in their token), most connections first. q filters by id. Connections without a user (publishable key only) are not listed.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"q","in":"query","required":false,"schema":{"type":"string","maxLength":128}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/users/{user}":{"get":{"operationId":"get_realtime_user","summary":"Get a connected user","description":"Whether a user is connected to a Realtime app, and for each of their connections: since when, from which origin, over WebSocket or stream, and the channels it listens to. Pair with kick_realtime_user to end them.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"user","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":128}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/databases":{"get":{"operationId":"list_realtime_databases","summary":"Databases for Realtime","description":"The account's PostgreSQL databases and whether each can stream changes to Realtime now (it must be running with external access on).","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/databases/{database_id}/tables":{"get":{"operationId":"list_realtime_database_tables","summary":"Tables for database events","description":"Tables of a PostgreSQL database (public schema) with their primary keys and whether changes from each are streamed now.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/database-events":{"get":{"operationId":"list_realtime_db_streams","summary":"List database events","description":"Databases whose row changes a Realtime app publishes, with their tables, events, channels, status and last event.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:read"]}],"x-cranl-scope":"realtime:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_realtime_db_stream","summary":"Stream database changes","description":"Publish every insert, update and delete in a CranL PostgreSQL database to a Realtime app as it happens: each change goes to \"<channel_prefix>.<table>\" (event insert/update/delete, data { table, op, key, row, old }). Installs a small trigger on the chosen tables (removed again when the stream is). The database must be running with external access on. Rows bigger than about 8 KB arrive as their key with truncated: true. Changes made while the gateway restarts are not replayed.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"database_id":{"type":"string","minLength":1,"description":"Database id"},"tables":{"description":"Table names, or [\"*\"] for every table (the default)","minItems":1,"maxItems":200,"type":"array","items":{"type":"string"}},"events":{"description":"Default all three","minItems":1,"type":"array","items":{"type":"string","enum":["insert","update","delete"]}},"channel_prefix":{"description":"Channels are \"<prefix>.<table>\"; default \"private:db\" (subscribers need a token allowing e.g. private:db.orders)","type":"string","maxLength":64},"row_channels":{"description":"Also publish each change to \"<prefix>.<table>.<key>\", for pages that watch one record","type":"boolean"},"exclude_columns":{"description":"Columns never sent, e.g. password_hash","maxItems":100,"type":"array","items":{"type":"string"}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["database_id"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/realtime/apps/{realtime_app_id}/database-events/{stream_id}":{"patch":{"operationId":"update_realtime_db_stream","summary":"Change database events","description":"Change which tables and events a database stream publishes, its channels and excluded columns, or pause it (enabled: false). Triggers move with the tables.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Stream id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"tables":{"description":"Table names, or [\"*\"] for every table (the default)","minItems":1,"maxItems":200,"type":"array","items":{"type":"string"}},"events":{"description":"Default all three","minItems":1,"type":"array","items":{"type":"string","enum":["insert","update","delete"]}},"channel_prefix":{"description":"Channels are \"<prefix>.<table>\"; default \"private:db\" (subscribers need a token allowing e.g. private:db.orders)","type":"string","maxLength":64},"row_channels":{"description":"Also publish each change to \"<prefix>.<table>.<key>\", for pages that watch one record","type":"boolean"},"exclude_columns":{"description":"Columns never sent, e.g. password_hash","maxItems":100,"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_realtime_db_stream","summary":"Stop database events","description":"Stop publishing a database's changes and remove the triggers no other stream needs.","tags":["realtime"],"security":[{"bearer":[]},{"oauth2":["realtime:write"]}],"x-cranl-scope":"realtime:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"realtime_app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Realtime app id (rt_app_…)"}},{"name":"stream_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Stream id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues":{"get":{"operationId":"get_queues_overview","summary":"Queues overview","description":"Whether this account's plan includes Queues (Basic, Pro and Enterprise do), its limits, this month's messages, and every queue with what is waiting, in progress and dead, and its last 24 hours.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"project_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_queue","summary":"Create queue","description":"Create a queue (Basic, Pro and Enterprise plans) with its first subscription, \"default\". Without an endpoint it is pull: workers receive from it with a queue key. With endpoint_url or target_app_id it is push: CranL POSTs each message there, signed. Next: create_queue_key (or connect_queue_to_app) so code can send to it.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63,"description":"Lowercase letters, digits, dots, dashes and underscores, e.g. emails or orders.paid"},"project_id":{"description":"Defaults to the account's first project","type":"string","minLength":1},"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"retention_seconds":{"description":"How long messages are kept (up to the plan's maximum). Default 3 days.","type":"integer","minimum":60,"maximum":9007199254740991},"max_message_kb":{"description":"The largest message, up to the plan's","type":"integer","minimum":1,"maximum":9007199254740991},"default_delay_seconds":{"description":"Every message waits this long unless it says otherwise","type":"integer","minimum":0,"maximum":604800},"masked_fields":{"description":"JSON fields shown as •••••• in the dashboard, e.g. password, token","maxItems":50,"type":"array","items":{"type":"string"}},"alert_backlog":{"description":"Alert when more messages than this are waiting","anyOf":[{"type":"integer","minimum":1,"maximum":9007199254740991},{"type":"null"}]},"alert_oldest_seconds":{"description":"Alert when the oldest waiting message is older than this","anyOf":[{"type":"integer","minimum":1,"maximum":9007199254740991},{"type":"null"}]},"alert_dead_letters":{"description":"Alert when there are more dead letters than this. Default 10.","anyOf":[{"type":"integer","minimum":1,"maximum":9007199254740991},{"type":"null"}]},"subscription":{"description":"The first subscription's settings","type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"mode":{"description":"pull: consumers receive (workers, SDK). push: CranL POSTs each message to an endpoint.","type":"string","enum":["pull","push"]},"endpoint_url":{"description":"Push: an https URL that accepts POSTs","anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"target_app_id":{"description":"Push: one of your CranL apps instead of a URL (its address plus endpoint_path)","anyOf":[{"type":"string"},{"type":"null"}]},"endpoint_path":{"description":"Push to an app: the path, default \"/api/queues\"","anyOf":[{"type":"string"},{"type":"null"}]},"worker_app_id":{"description":"The worker that drains this subscription (shown together; used by autoscaling)","anyOf":[{"type":"string"},{"type":"null"}]},"concurrency":{"description":"Push: requests in flight at once. Default 10.","type":"integer","minimum":1,"maximum":500},"rate_per_second":{"description":"Push: at most this many messages a second","anyOf":[{"type":"integer","minimum":1,"maximum":10000},{"type":"null"}]},"batch_size":{"description":"Push: messages per request (more than 1 sends { messages: [...] })","type":"integer","minimum":1,"maximum":100},"timeout_seconds":{"description":"Push: seconds to wait for an answer. Default 30.","type":"integer","minimum":1,"maximum":900},"visibility_seconds":{"description":"Pull: how long a received message is hidden before it is retried. Default 60.","type":"integer","minimum":5,"maximum":43200},"max_attempts":{"description":"Tries before a message becomes a dead letter. Default 10.","type":"integer","minimum":1,"maximum":100},"backoff":{"type":"string","enum":["exponential","linear","fixed"]},"backoff_initial_seconds":{"type":"integer","minimum":1,"maximum":86400},"backoff_max_seconds":{"type":"integer","minimum":1,"maximum":604800},"filter":{"description":"Only messages whose headers match every condition, e.g. [{ \"header\": \"type\", \"op\": \"prefix\", \"value\": \"order.\" }]","maxItems":10,"type":"array","items":{"type":"object","properties":{"header":{"type":"string"},"op":{"type":"string","enum":["eq","neq","in","prefix","exists","not_exists"]},"value":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["header","op"]}},"paused":{"description":"Paused subscriptions keep their messages but deliver none","type":"boolean"},"circuit_threshold":{"description":"Push: failures in a row before CranL pauses it and tells you (0 never). Default 50.","type":"integer","minimum":0,"maximum":100000}}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}":{"get":{"operationId":"get_queue","summary":"Get queue","description":"One queue: settings, its API endpoint, and each subscription with its mode, endpoint, health and counts (waiting, scheduled, in progress, dead).","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"update_queue","summary":"Update queue","description":"Change a queue's retention, largest message, default delay, masked fields and alert thresholds, or pause sending to it (publishing_paused: senders get 409 until resumed).","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"retention_seconds":{"description":"How long messages are kept (up to the plan's maximum). Default 3 days.","type":"integer","minimum":60,"maximum":9007199254740991},"max_message_kb":{"description":"The largest message, up to the plan's","type":"integer","minimum":1,"maximum":9007199254740991},"default_delay_seconds":{"description":"Every message waits this long unless it says otherwise","type":"integer","minimum":0,"maximum":604800},"masked_fields":{"description":"JSON fields shown as •••••• in the dashboard, e.g. password, token","maxItems":50,"type":"array","items":{"type":"string"}},"alert_backlog":{"description":"Alert when more messages than this are waiting","anyOf":[{"type":"integer","minimum":1,"maximum":9007199254740991},{"type":"null"}]},"alert_oldest_seconds":{"description":"Alert when the oldest waiting message is older than this","anyOf":[{"type":"integer","minimum":1,"maximum":9007199254740991},{"type":"null"}]},"alert_dead_letters":{"description":"Alert when there are more dead letters than this. Default 10.","anyOf":[{"type":"integer","minimum":1,"maximum":9007199254740991},{"type":"null"}]},"publishing_paused":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_queue","summary":"Delete queue","description":"Delete a queue and every message in it. Keys made only for it are revoked. This cannot be undone.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:delete"]}],"x-cranl-scope":"queues:delete","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/stats":{"get":{"operationId":"get_queue_stats","summary":"Queue stats","description":"Sent, delivered, acknowledged, failed and dead per minute (1h), per 15 minutes (24h) or per hour (7d, as far as kept), the average time from send to done, and each subscription's counts.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"range","in":"query","required":false,"schema":{"type":"string","enum":["1h","24h","7d"]}},{"name":"subscription","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/subscriptions":{"post":{"operationId":"create_queue_subscription","summary":"Add a subscription","description":"Add a subscription: every message sent to the queue (that passes its filter) is delivered to it too, separately from the others (fan-out). include_existing: true also delivers the messages the queue still keeps.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"mode":{"description":"pull: consumers receive (workers, SDK). push: CranL POSTs each message to an endpoint.","type":"string","enum":["pull","push"]},"endpoint_url":{"description":"Push: an https URL that accepts POSTs","anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"target_app_id":{"description":"Push: one of your CranL apps instead of a URL (its address plus endpoint_path)","anyOf":[{"type":"string"},{"type":"null"}]},"endpoint_path":{"description":"Push to an app: the path, default \"/api/queues\"","anyOf":[{"type":"string"},{"type":"null"}]},"worker_app_id":{"description":"The worker that drains this subscription (shown together; used by autoscaling)","anyOf":[{"type":"string"},{"type":"null"}]},"concurrency":{"description":"Push: requests in flight at once. Default 10.","type":"integer","minimum":1,"maximum":500},"rate_per_second":{"description":"Push: at most this many messages a second","anyOf":[{"type":"integer","minimum":1,"maximum":10000},{"type":"null"}]},"batch_size":{"description":"Push: messages per request (more than 1 sends { messages: [...] })","type":"integer","minimum":1,"maximum":100},"timeout_seconds":{"description":"Push: seconds to wait for an answer. Default 30.","type":"integer","minimum":1,"maximum":900},"visibility_seconds":{"description":"Pull: how long a received message is hidden before it is retried. Default 60.","type":"integer","minimum":5,"maximum":43200},"max_attempts":{"description":"Tries before a message becomes a dead letter. Default 10.","type":"integer","minimum":1,"maximum":100},"backoff":{"type":"string","enum":["exponential","linear","fixed"]},"backoff_initial_seconds":{"type":"integer","minimum":1,"maximum":86400},"backoff_max_seconds":{"type":"integer","minimum":1,"maximum":604800},"filter":{"description":"Only messages whose headers match every condition, e.g. [{ \"header\": \"type\", \"op\": \"prefix\", \"value\": \"order.\" }]","maxItems":10,"type":"array","items":{"type":"object","properties":{"header":{"type":"string"},"op":{"type":"string","enum":["eq","neq","in","prefix","exists","not_exists"]},"value":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["header","op"]}},"paused":{"description":"Paused subscriptions keep their messages but deliver none","type":"boolean"},"circuit_threshold":{"description":"Push: failures in a row before CranL pauses it and tells you (0 never). Default 50.","type":"integer","minimum":0,"maximum":100000},"include_existing":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/subscriptions/{subscription_id}":{"patch":{"operationId":"update_queue_subscription","summary":"Update a subscription","description":"Change how a subscription delivers: push or pull, endpoint, concurrency, rate, batch size, timeouts, attempts, backoff and filter. paused: false resumes one the circuit breaker paused.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Subscription id or name"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"mode":{"description":"pull: consumers receive (workers, SDK). push: CranL POSTs each message to an endpoint.","type":"string","enum":["pull","push"]},"endpoint_url":{"description":"Push: an https URL that accepts POSTs","anyOf":[{"type":"string","format":"uri"},{"type":"null"}]},"target_app_id":{"description":"Push: one of your CranL apps instead of a URL (its address plus endpoint_path)","anyOf":[{"type":"string"},{"type":"null"}]},"endpoint_path":{"description":"Push to an app: the path, default \"/api/queues\"","anyOf":[{"type":"string"},{"type":"null"}]},"worker_app_id":{"description":"The worker that drains this subscription (shown together; used by autoscaling)","anyOf":[{"type":"string"},{"type":"null"}]},"concurrency":{"description":"Push: requests in flight at once. Default 10.","type":"integer","minimum":1,"maximum":500},"rate_per_second":{"description":"Push: at most this many messages a second","anyOf":[{"type":"integer","minimum":1,"maximum":10000},{"type":"null"}]},"batch_size":{"description":"Push: messages per request (more than 1 sends { messages: [...] })","type":"integer","minimum":1,"maximum":100},"timeout_seconds":{"description":"Push: seconds to wait for an answer. Default 30.","type":"integer","minimum":1,"maximum":900},"visibility_seconds":{"description":"Pull: how long a received message is hidden before it is retried. Default 60.","type":"integer","minimum":5,"maximum":43200},"max_attempts":{"description":"Tries before a message becomes a dead letter. Default 10.","type":"integer","minimum":1,"maximum":100},"backoff":{"type":"string","enum":["exponential","linear","fixed"]},"backoff_initial_seconds":{"type":"integer","minimum":1,"maximum":86400},"backoff_max_seconds":{"type":"integer","minimum":1,"maximum":604800},"filter":{"description":"Only messages whose headers match every condition, e.g. [{ \"header\": \"type\", \"op\": \"prefix\", \"value\": \"order.\" }]","maxItems":10,"type":"array","items":{"type":"object","properties":{"header":{"type":"string"},"op":{"type":"string","enum":["eq","neq","in","prefix","exists","not_exists"]},"value":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]}},"required":["header","op"]}},"paused":{"description":"Paused subscriptions keep their messages but deliver none","type":"boolean"},"circuit_threshold":{"description":"Push: failures in a row before CranL pauses it and tells you (0 never). Default 50.","type":"integer","minimum":0,"maximum":100000},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_queue_subscription","summary":"Delete a subscription","description":"Remove a subscription and the messages waiting for it. The queue keeps its other subscriptions; it must keep at least one.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:delete"]}],"x-cranl-scope":"queues:delete","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Subscription id or name"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/subscriptions/{subscription_id}/secret":{"get":{"operationId":"get_queue_signing_secret","summary":"Get a push signing secret","description":"The secret pushed requests are signed with (CranL-Signature: t=…,v1=…, an HMAC-SHA256 of `t.message_id.body`). Your endpoint checks it to know the request came from CranL. The SDKs verify it for you.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:secrets"]}],"x-cranl-scope":"queues:secrets","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Subscription id or name"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/subscriptions/{subscription_id}/secret/rotate":{"post":{"operationId":"rotate_queue_signing_secret","summary":"Rotate a push signing secret","description":"Issue a new signing secret. For a day both sign each request, so your endpoint can switch without rejecting anything.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:secrets"]}],"x-cranl-scope":"queues:secrets","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"subscription_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Subscription id or name"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/messages":{"post":{"operationId":"send_queue_message","summary":"Send a message","description":"Send one message ({ body, … }) or up to 100 ({ messages: [...] }). The same checks as the queues API: the plan's rate and month, the queue's size limit. Apps send with a queue key at queues.cranl.net instead.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"description":"Any JSON value (stored as JSON) or a string (stored as text)"},"body_base64":{"description":"Bytes, instead of body","type":"string"},"content_type":{"type":"string"},"headers":{"description":"Metadata; also what subscription filters look at","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"delay":{"description":"Seconds before it may be delivered","type":"integer","minimum":0,"maximum":604800},"not_before":{"description":"Or an ISO time","type":"string"},"dedupe_key":{"description":"The same key within retention is stored once","type":"string","maxLength":200},"ordering_key":{"description":"Messages with the same key are delivered one at a time, in order","type":"string","maxLength":200},"priority":{"description":"Higher first. Default 0.","type":"integer","minimum":-10,"maximum":10},"retention":{"description":"Seconds to keep it, at most the queue's retention","type":"integer","minimum":60,"maximum":9007199254740991},"messages":{"maxItems":100,"type":"array","items":{"type":"object","properties":{"body":{"description":"Any JSON value (stored as JSON) or a string (stored as text)"},"body_base64":{"description":"Bytes, instead of body","type":"string"},"content_type":{"type":"string"},"headers":{"description":"Metadata; also what subscription filters look at","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"delay":{"description":"Seconds before it may be delivered","type":"integer","minimum":0,"maximum":604800},"not_before":{"description":"Or an ISO time","type":"string"},"dedupe_key":{"description":"The same key within retention is stored once","type":"string","maxLength":200},"ordering_key":{"description":"Messages with the same key are delivered one at a time, in order","type":"string","maxLength":200},"priority":{"description":"Higher first. Default 0.","type":"integer","minimum":-10,"maximum":10},"retention":{"description":"Seconds to keep it, at most the queue's retention","type":"integer","minimum":60,"maximum":9007199254740991}}}}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"list_queue_messages","summary":"Browse messages","description":"A queue's deliveries, newest first, with a short preview (masked fields hidden). Filter by state (ready, scheduled, leased, done, dead), subscription, or search by message id, delivery id, dedupe key, ordering key or a header value. Page with before: the last delivery_id.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"state","in":"query","required":false,"schema":{"type":"string","enum":["ready","scheduled","leased","done","dead"]}},{"name":"subscription","in":"query","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","required":false,"schema":{"type":"string","maxLength":200}},{"name":"error_group","in":"query","required":false,"schema":{"description":"Only deliveries failing with this error group (from get_queue_errors).","type":"string","maxLength":400},"description":"Only deliveries failing with this error group (from get_queue_errors)."},{"name":"before","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/messages/{message_id}":{"get":{"operationId":"get_queue_message","summary":"Get a message","description":"One message: body (masked fields hidden), headers, keys, and for each subscription its state and every attempt with its outcome, HTTP status and error.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/messages/retry":{"post":{"operationId":"retry_queue_messages","summary":"Deliver now","description":"Deliver these now: a scheduled, waiting or dead delivery moves to the front (a dead one gets fresh attempts).","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"delivery_ids":{"minItems":1,"maxItems":1000,"type":"array","items":{"type":"string","pattern":"^\\d+$"}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["delivery_ids"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/messages/discard":{"post":{"operationId":"discard_queue_messages","summary":"Discard messages","description":"Remove these deliveries (not ones being worked on right now). The message stays for other subscriptions.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:delete"]}],"x-cranl-scope":"queues:delete","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"delivery_ids":{"minItems":1,"maxItems":1000,"type":"array","items":{"type":"string","pattern":"^\\d+$"}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["delivery_ids"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/dead-letters/redrive":{"post":{"operationId":"redrive_queue_dead_letters","summary":"Send dead letters back","description":"Give dead letters a fresh set of attempts: all of them, one subscription's, or the delivery ids named. Fix the cause first, or they will fail again.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"type":"string"},"delivery_ids":{"maxItems":1000,"type":"array","items":{"type":"string","pattern":"^\\d+$"}},"error_group":{"description":"Only the dead letters failing with this error group (from get_queue_errors).","type":"string","maxLength":400},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/dead-letters/move":{"post":{"operationId":"move_queue_dead_letters","summary":"Move dead letters","description":"Send dead letters (the ones named, or the newest 100) to another queue and remove them here, e.g. into a queue a fixed or manual consumer drains.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"target_queue":{"type":"string","minLength":1},"delivery_ids":{"maxItems":1000,"type":"array","items":{"type":"string","pattern":"^\\d+$"}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["target_queue"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/purge":{"post":{"operationId":"purge_queue","summary":"Purge a queue","description":"Delete messages in the states named (ready, scheduled, dead, done), of one subscription or all. Messages being worked on are left alone. This cannot be undone.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:delete"]}],"x-cranl-scope":"queues:delete","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"states":{"minItems":1,"type":"array","items":{"type":"string","enum":["ready","scheduled","dead","done"]}},"subscription":{"type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["states"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/replay":{"post":{"operationId":"replay_queue_messages","summary":"Replay messages","description":"Deliver again what a subscription already handled, for messages sent between from and to that the queue still keeps. Useful after fixing a bug in a consumer.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"type":"string","minLength":1},"from":{"type":"string","description":"ISO time"},"to":{"type":"string","description":"ISO time"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["subscription","from","to"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/schedules":{"get":{"operationId":"list_queue_schedules","summary":"List schedules","description":"A queue's schedules: messages sent on a cron, with the next and last run and the last error.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_queue_schedule","summary":"Create a schedule","description":"Send a message on a cron, in a time zone (replaces a cron job that only enqueues work).","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"cron":{"type":"string"},"timezone":{"description":"IANA time zone, default UTC","type":"string"},"body":{"description":"The message: any JSON value or a string, up to 64 KB"},"content_type":{"type":"string"},"headers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"enabled":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","cron"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/schedules/{schedule_id}":{"patch":{"operationId":"update_queue_schedule","summary":"Update a schedule","description":"Change a schedule's cron, time zone, message, or turn it off.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"schedule_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Schedule id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"cron":{"description":"Five fields, e.g. \"*/5 * * * *\" or \"0 9 * * 1-5\"","type":"string"},"timezone":{"description":"IANA time zone, default UTC","type":"string"},"body":{"description":"The message: any JSON value or a string, up to 64 KB"},"content_type":{"type":"string"},"headers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"enabled":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_queue_schedule","summary":"Delete a schedule","description":"Stop and remove a schedule.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:delete"]}],"x-cranl-scope":"queues:delete","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"schedule_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Schedule id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/schedules/{schedule_id}/run":{"post":{"operationId":"run_queue_schedule","summary":"Run a schedule now","description":"Send a schedule's message now, outside its cron.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"schedule_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Schedule id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/inboxes":{"get":{"operationId":"list_queue_inboxes","summary":"List inboxes","description":"A queue's inboxes: public URLs that turn any incoming webhook (Stripe, GitHub, a partner) into a message, with how many were received and rejected. The URLs are secrets: they are returned only to callers who may change the queue (queues:write, not a viewer).","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_queue_inbox","summary":"Create an inbox","description":"A URL to give a webhook sender. Each request becomes a message (body as sent, useful headers kept, the sender's delivery id as dedupe key), answered 202 at once so slow processing never makes the sender retry. Set verify to check signatures.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"verify":{"description":"How the sender proves it is them","type":"string","enum":["none","hmac","token","stripe","github"]},"verify_header":{"description":"hmac: the signature header (default X-Signature); token: the header carrying it (default Authorization)","anyOf":[{"type":"string"},{"type":"null"}]},"verify_secret":{"description":"The sender's signing secret (Stripe's whsec_…, GitHub's webhook secret, …)","anyOf":[{"type":"string"},{"type":"null"}]},"enabled":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/inboxes/{inbox_id}":{"patch":{"operationId":"update_queue_inbox","summary":"Update an inbox","description":"Change an inbox's verification or turn it off. rotate_url: true gives it a new URL (the old one stops at once).","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"inbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Inbox id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"verify":{"description":"How the sender proves it is them","type":"string","enum":["none","hmac","token","stripe","github"]},"verify_header":{"description":"hmac: the signature header (default X-Signature); token: the header carrying it (default Authorization)","anyOf":[{"type":"string"},{"type":"null"}]},"verify_secret":{"description":"The sender's signing secret (Stripe's whsec_…, GitHub's webhook secret, …)","anyOf":[{"type":"string"},{"type":"null"}]},"enabled":{"type":"boolean"},"rotate_url":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_queue_inbox","summary":"Delete an inbox","description":"Remove an inbox; its URL stops accepting requests at once.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:delete"]}],"x-cranl-scope":"queues:delete","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"inbox_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Inbox id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queue-keys":{"get":{"operationId":"list_queue_keys","summary":"List queue keys","description":"Keys for the queues API: their permissions (send, receive, manage), which queues, allowed addresses, expiry and when each was last used. The keys themselves are never shown again.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_queue_key","summary":"Create a queue key","description":"A key for queues.cranl.net (Authorization: Bearer cq_sk_…). Give it only what it needs: send for producers, receive for workers, on the queues named (none means every queue). allowed_ips limits where it works from. Returned once.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:secrets"]}],"x-cranl-scope":"queues:secrets","x-cranl-annotations":{},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63},"permissions":{"minItems":1,"type":"array","items":{"type":"string","enum":["send","receive","manage"]}},"queues":{"description":"Queue ids or names; none means every queue of the account","maxItems":100,"type":"array","items":{"type":"string"}},"allowed_ips":{"description":"IPs or CIDR ranges, e.g. 203.0.113.0/24","maxItems":20,"type":"array","items":{"type":"string"}},"expires_at":{"description":"ISO time","anyOf":[{"type":"string"},{"type":"null"}]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","permissions"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queue-keys/{key_id}":{"delete":{"operationId":"revoke_queue_key","summary":"Revoke a queue key","description":"Stop a queue key working, at once and everywhere.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:secrets"]}],"x-cranl-scope":"queues:secrets","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Key id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/connect":{"post":{"operationId":"connect_queue_to_app","summary":"Connect a queue to an app","description":"Give one of your apps (or workers) a key scoped to this queue and put CRANL_QUEUES_URL and CRANL_QUEUES_KEY in its environment, then restart it. send lets it send, receive lets it work through messages. An app connected to several queues keeps one key whose scope grows.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:secrets"]}],"x-cranl-scope":"queues:secrets","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"app_id":{"type":"string","minLength":1,"description":"The CranL application or worker"},"permissions":{"description":"Default: send","minItems":1,"type":"array","items":{"type":"string","enum":["send","receive"]}},"restart":{"description":"Default true.","type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["app_id"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/connections":{"get":{"operationId":"list_queue_connections","summary":"List a queue's connected apps","description":"The apps wired to a queue: those holding a key for it (sends, receives), those its push subscriptions deliver to, and the workers named on its subscriptions, with when each key was last used.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/connections/{app_id}":{"delete":{"operationId":"disconnect_queue_from_app","summary":"Disconnect an app from a queue","description":"Take this queue out of the app's key, so the app can no longer send to or receive from it; its other queues keep working. Its environment variables are left as they are.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:secrets"]}],"x-cranl-scope":"queues:secrets","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/workers":{"get":{"operationId":"list_workers","summary":"List workers","description":"Your workers (apps with no public address that run all the time): replicas, restart policy and autoscaling.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/workers/{app_id}":{"patch":{"operationId":"update_worker","summary":"Scale a worker","description":"Set a worker's replicas (0 to 20), restart policy (always, on-failure with max_restarts, never) and shutdown grace, or turn on autoscaling: replicas follow a queue subscription's backlog (waiting plus in progress ÷ per_replica), between min and max, down to zero if scale_to_zero.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Worker application id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"replicas":{"type":"integer","minimum":0,"maximum":20},"restart_policy":{"type":"string","enum":["always","on-failure","never"]},"max_restarts":{"type":"integer","minimum":1,"maximum":1000},"stop_grace_seconds":{"description":"Time to finish current messages after SIGTERM","type":"integer","minimum":1,"maximum":3600},"autoscale":{"anyOf":[{"type":"object","properties":{"enabled":{"type":"boolean"},"queue":{"description":"Queue id or name","type":"string"},"subscription":{"description":"Default \"default\"","type":"string"},"subscription_id":{"type":"string"},"min":{"type":"integer","minimum":0,"maximum":20},"max":{"type":"integer","minimum":1,"maximum":20},"per_replica":{"description":"Messages one replica handles; default 100","type":"integer","minimum":1,"maximum":9007199254740991},"scale_to_zero":{"type":"boolean"}}},{"type":"null"}]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/errors":{"get":{"operationId":"get_queue_errors","summary":"What is failing on a queue","description":"Deliveries that are failing or dead, grouped by their error (ids and long numbers taken out so the same failure groups together), biggest first: an example, the HTTP status, the subscription, how many are dead and how many are still being retried. Pass a group's `group` to list_queue_messages (error_group) to see them, or to redrive_queue_dead_letters to send them back.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"subscription","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/messages/{message_id}/resend":{"post":{"operationId":"resend_queue_message","summary":"Send a message again","description":"Send a copy of a message, as it was or with a new body or headers, e.g. a dead letter whose payload was wrong. In an edited JSON body, masked fields left as •••••• keep their original value. discard_delivery_ids removes the original's dead letters once the copy is sent. The copy carries the header cranl-resent-from.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"body":{"description":"A replacement body: any JSON value, or a string."},"headers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"delay":{"type":"integer","minimum":0,"maximum":604800},"discard_delivery_ids":{"maxItems":1000,"type":"array","items":{"type":"string","pattern":"^\\d+$"}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/messages/{message_id}/curl":{"post":{"operationId":"get_queue_message_curl","summary":"A message as a signed curl command","description":"A curl command that POSTs a message to a URL (your machine, a staging app) exactly as a push from a subscription would arrive, with a valid CranL-Signature for five minutes. Masked fields stay masked in the body. Text bodies only.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:secrets"]}],"x-cranl-scope":"queues:secrets","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"message_id","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","minLength":1,"maxLength":500},"subscription":{"type":"string"}},"required":["url"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/delivery/pause":{"post":{"operationId":"pause_queue_delivery","summary":"Pause a queue's delivery","description":"Pause delivery on every subscription of a queue at once, e.g. during an incident. Messages keep arriving and wait; nothing is lost. resume_queue_delivery starts it again.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/delivery/resume":{"post":{"operationId":"resume_queue_delivery","summary":"Resume a queue's delivery","description":"Resume delivery on every subscription of a queue, including ones paused after repeated failures; what waited is delivered in order.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:write"]}],"x-cranl-scope":"queues:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queue-usage":{"get":{"operationId":"get_queues_usage","summary":"Queue usage by day","description":"Messages sent and delivered per day (7 to 90 days, default 30) for the account or one queue, the month so far, where the month is heading at this pace, the plan's monthly limit, and the busiest queues.","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","minimum":7,"maximum":90}},{"name":"queue_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queues/{queue_id}/activity":{"get":{"operationId":"get_queue_activity","summary":"A queue's activity","description":"What happened to a queue, newest first: changes (operation, who, and whether through the dashboard, the API or a connected app) and what CranL raised (alerts, a paused endpoint, recovery).","tags":["queues"],"security":[{"bearer":[]},{"oauth2":["queues:read"]}],"x-cranl-scope":"queues:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"queue_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Queue id or name"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/backends":{"get":{"operationId":"list_backends","summary":"List managed backends","description":"Managed Supabase and Convex backends in this account.","tags":["backends"],"security":[{"bearer":[]},{"oauth2":["backends:read"]}],"x-cranl-scope":"backends:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"backends":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["backends"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_backend","summary":"Create managed backend","description":"Provision a managed Supabase (Postgres, auth, storage, realtime) or Convex backend. Provisioning takes a few minutes; poll get_backend until status is running.","tags":["backends"],"security":[{"bearer":[]},{"oauth2":["backends:write"]}],"x-cranl-scope":"backends:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["supabase","convex"]},"name":{"type":"string","maxLength":60},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["provider"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/backends/{backend_id}":{"get":{"operationId":"get_backend","summary":"Get managed backend","description":"A backend's status, API URL and credentials. Secret values are masked unless reveal: true (needs the env:reveal scope).","tags":["backends"],"security":[{"bearer":[]},{"oauth2":["backends:read"]}],"x-cranl-scope":"backends:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"backend_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Backend id"}},{"name":"reveal","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_backend","summary":"Delete managed backend","description":"Delete the backend and all its data. Not recoverable. Destructive: needs a confirmation token.","tags":["backends"],"security":[{"bearer":[]},{"oauth2":["backends:write"]}],"x-cranl-scope":"backends:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"backend_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Backend id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/backends/{backend_id}/{action}":{"post":{"operationId":"backend_lifecycle","summary":"Start, stop, restart or rotate keys","description":"rotate-keys issues new API keys; apps using the old keys stop working until their environment is updated.","tags":["backends"],"security":[{"bearer":[]},{"oauth2":["backends:write"]}],"x-cranl-scope":"backends:write","x-cranl-annotations":{},"parameters":[{"name":"backend_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Backend id"}},{"name":"action","in":"path","required":true,"schema":{"type":"string","enum":["start","stop","restart","rotate-keys"]}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"message":{"type":"string"}},"required":["ok","message"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/previews":{"post":{"operationId":"create_preview","summary":"Create preview deployment","description":"Deploy another branch of a Git-based app as its own app (same region, build settings and, by default, the same environment variables) at its own URL. Use for pull requests, or with ttl_hours as a temporary copy of production to test something (branch: main, ttl_hours: 2); it is removed automatically when the time is up. delete_preview removes it earlier. Mind that copied variables point at the same databases as the parent.","tags":["previews"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Parent application id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"branch":{"type":"string","minLength":1},"copy_env":{"default":true,"type":"boolean"},"env":{"description":"Variables to set or override on the preview.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"ttl_hours":{"description":"Remove the preview automatically after this many hours.","type":"integer","minimum":1,"maximum":720},"wait":{"type":"boolean"},"timeout_seconds":{"type":"integer","minimum":10,"maximum":900},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["branch"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}},"operation":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false},"reused":{"type":"boolean"}},"required":["app","operation","reused"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"list_previews","summary":"List previews","description":"Preview deployments of an app, one per branch.","tags":["previews"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Parent application id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"previews":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}}}},"required":["previews"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/previews/{preview_id}":{"delete":{"operationId":"delete_preview","summary":"Delete preview","description":"Remove a preview app right away. Only previews can be removed this way; production apps go through delete_app.","tags":["previews"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"preview_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Preview application id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/cron":{"get":{"operationId":"list_cron_jobs","summary":"List cron jobs","description":"Commands scheduled to run inside an app's container.","tags":["cron"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"jobs":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["jobs"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_cron_job","summary":"Create cron job","description":"Run a command inside the app's container on a schedule (five-field cron expression, e.g. \"0 3 * * *\").","tags":["cron"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"cron":{"type":"string","description":"minute hour day-of-month month day-of-week"},"command":{"type":"string","minLength":1},"shell":{"type":"string","enum":["bash","sh"]},"timezone":{"description":"IANA name, e.g. UTC or Asia/Riyadh.","type":"string"},"enabled":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name","cron","command"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"job":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["job"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/cron/{job_id}":{"delete":{"operationId":"delete_cron_job","summary":"Delete cron job","description":"Stop and remove a scheduled command.","tags":["cron"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"job_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Cron job id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/cron/{job_id}/run":{"post":{"operationId":"run_cron_job","summary":"Run cron job now","description":"Run a scheduled command once, now. Its output appears in get_app_logs.","tags":["cron"],"security":[{"bearer":[]},{"oauth2":["apps:deploy"]}],"x-cranl-scope":"apps:deploy","x-cranl-annotations":{},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"job_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Cron job id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/volumes":{"get":{"operationId":"list_volumes","summary":"List volumes","description":"Persistent storage attached to an app: named volumes, host paths, and files written into the container.","tags":["cron"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"volumes":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["volumes"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"add_volume","summary":"Add volume","description":"Persist a directory across deploys (type volume), or write a file into the container (type file, with content). Takes effect on the next deploy.","tags":["cron"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"default":"volume","type":"string","enum":["volume","bind","file"]},"mount_path":{"type":"string","description":"Path inside the container, e.g. /data"},"volume_name":{"type":"string"},"file_path":{"description":"For type file: the file name under mount_path.","type":"string"},"content":{"description":"For type file: the file's content.","type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["mount_path"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"note":{"type":"string"}},"required":["ok","note"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/volumes/{volume_id}":{"delete":{"operationId":"remove_volume","summary":"Remove volume","description":"Detach persistent storage from the app. Data in a named volume is deleted. Destructive: needs a confirmation token.","tags":["cron"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"volume_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Volume id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/blueprint":{"get":{"operationId":"export_blueprint","summary":"Export blueprint","description":"Describe a project's apps, databases, domains and cron jobs as cranl.json, ready to commit next to the code. Environment values are left out (null) unless include_env_values (needs env:reveal). Databases need db:read and variable names env:read; what the credential may not read is left out and named in `omitted`.","tags":["blueprints"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"project_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"include_env_values","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"blueprint":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"filename":{"type":"string"},"omitted":{"type":"array","items":{"type":"string"}}},"required":["blueprint","filename"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/blueprint/plan":{"post":{"operationId":"plan_blueprint","summary":"Plan blueprint","description":"Compare cranl.json with what exists and list the changes apply_blueprint would make. Nothing is changed. Resources the file does not mention are listed as unmanaged and are never deleted. Changes to a protected app are listed under protected: apply_blueprint refuses a file that has any. Reads through the same scopes as the single-purpose operations: a file naming databases needs db:read; env names need env:read, and env values are only compared with the stored ones with env:reveal (without it every supplied value is listed as set).","tags":["blueprints"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"blueprint":{"anyOf":[{"type":"object","properties":{"$schema":{"type":"string"},"version":{"default":1,"type":"number","const":1},"project":{"description":"Project name. Created if missing; default: the account's first project.","type":"string"},"region":{"description":"Default region for apps and databases.","type":"string"},"apps":{"default":{},"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"repository":{"description":"owner/name of a connected GitHub repository.","type":"string"},"git_url":{"type":"string","format":"uri"},"image":{"description":"A Docker image, instead of a repository.","type":"string"},"path":{"description":"A directory deployed from local source with `cranl deploy <path>` (no Git; add --static when static is true). apply_blueprint manages its settings; the code itself is uploaded by the CLI.","type":"string"},"static":{"description":"With path: serve the files as a static site.","type":"boolean"},"branch":{"type":"string"},"build":{"type":"string","enum":["auto","dockerfile"]},"build_path":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"region":{"type":"string"},"env":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]},"description":"A string sets the value; null means \"must exist, value managed elsewhere\"."},"domains":{"type":"array","items":{"type":"string"}},"databases":{"description":"Databases (by name) to connect; each sets a connection-URL variable.","type":"array","items":{"anyOf":[{"type":"string"},{"type":"object","properties":{"name":{"type":"string"},"variable":{"type":"string"}},"required":["name"]}]}},"cron":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"schedule":{"type":"string"},"command":{"type":"string"},"timezone":{"type":"string"}},"required":["name","schedule","command"]}}}}},"databases":{"default":{},"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"type":{"type":"string","enum":["postgresql","mysql","mariadb","mongodb","redis"]},"region":{"type":"string"}},"required":["type"]}}}},{"type":"string"}],"description":"The cranl.json content, as an object or a JSON string."}},"required":["blueprint"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/blueprint/apply":{"post":{"operationId":"apply_blueprint","summary":"Apply blueprint","description":"Make the project match cranl.json: create missing apps and databases, connect them, set env values, add domains and cron jobs. Only adds and updates, never deletes. Every step is held to the same scope, protection and spend checks as calling it directly: a file that would change a protected app is refused before anything is changed (plan_blueprint shows which). Stops at the first failure and reports what was done; running it again continues from there.","tags":["blueprints"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"blueprint":{"anyOf":[{"type":"object","properties":{"$schema":{"type":"string"},"version":{"default":1,"type":"number","const":1},"project":{"description":"Project name. Created if missing; default: the account's first project.","type":"string"},"region":{"description":"Default region for apps and databases.","type":"string"},"apps":{"default":{},"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"repository":{"description":"owner/name of a connected GitHub repository.","type":"string"},"git_url":{"type":"string","format":"uri"},"image":{"description":"A Docker image, instead of a repository.","type":"string"},"path":{"description":"A directory deployed from local source with `cranl deploy <path>` (no Git; add --static when static is true). apply_blueprint manages its settings; the code itself is uploaded by the CLI.","type":"string"},"static":{"description":"With path: serve the files as a static site.","type":"boolean"},"branch":{"type":"string"},"build":{"type":"string","enum":["auto","dockerfile"]},"build_path":{"type":"string"},"port":{"type":"integer","minimum":1,"maximum":65535},"region":{"type":"string"},"env":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"string"},{"type":"null"}]},"description":"A string sets the value; null means \"must exist, value managed elsewhere\"."},"domains":{"type":"array","items":{"type":"string"}},"databases":{"description":"Databases (by name) to connect; each sets a connection-URL variable.","type":"array","items":{"anyOf":[{"type":"string"},{"type":"object","properties":{"name":{"type":"string"},"variable":{"type":"string"}},"required":["name"]}]}},"cron":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"schedule":{"type":"string"},"command":{"type":"string"},"timezone":{"type":"string"}},"required":["name","schedule","command"]}}}}},"databases":{"default":{},"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"type":{"type":"string","enum":["postgresql","mysql","mariadb","mongodb","redis"]},"region":{"type":"string"}},"required":["type"]}}}},{"type":"string"}],"description":"The cranl.json content, as an object or a JSON string."},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["blueprint"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/agents-md":{"get":{"operationId":"generate_agents_md","summary":"Generate AGENTS.md section","description":"A Markdown section for the repository's AGENTS.md (or CLAUDE.md) telling coding agents how this app is deployed on CranL: app id, URL, how to deploy, check logs and roll back.","tags":["blueprints"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"markdown":{"type":"string"},"filename":{"type":"string"}},"required":["markdown","filename"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/events":{"get":{"operationId":"list_events","summary":"List events","description":"Events in this account after a cursor, oldest first: deploy.succeeded (A deploy finished and the app is live.); deploy.failed (A deploy failed. Data carries the failing lines and a suggested fix.); deploy.rolled_back (CranL rolled an app back automatically after the new version kept failing.); app.down (An uptime monitor found the app not answering.); app.up (An app that was down answers again.); approval.requested (An agent is waiting for a person to approve an action.); approval.decided (An approval request was approved or denied.); resource.deleted (An app or database was scheduled for deletion.); resource.expired (A temporary app reached its expiry and was removed.); backup.failed (A database backup failed. Data carries the database, the backup id and the reason.); backup.late (A database's scheduled backups stopped arriving: none finished in twice the interval.); backup.restored (A backup was restored, into the database itself or into a new one (target_database_id).); backup.restore_failed (Restoring a backup failed. Data carries the reason; a replaced database keeps its data or its safety backup.); queue.backlog (A queue's waiting messages passed its alert threshold. Data carries the queue, the subscription and the count.); queue.stalled (A queue's oldest waiting message is older than its alert threshold: nothing is draining it.); queue.dead_letters (Dead letters on a queue passed its alert threshold. Data carries the count and the last error.); queue.endpoint_paused (A push subscription kept failing and CranL paused it. Data carries the endpoint and the last error.); queue.recovered (A queue that raised an alert is back under its thresholds.); claim.completed (A claimable deploy was claimed by a user.); agent_run.completed (A cloud agent run finished its task. Data carries the summary, the diff size and the pull request.); agent_run.failed (A cloud agent run failed. Data carries the reason.); agent_run.stopped (A cloud agent run was stopped, by a person or by its time or model budget.). Without after, the most recent events; pass the last id you saw as after to get only newer ones.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:read"]}],"x-cranl-scope":"events:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"after","in":"query","required":false,"schema":{"description":"Return events with a larger id than this.","type":"string"},"description":"Return events with a larger id than this."},{"name":"types","in":"query","required":false,"schema":{"description":"Only these event types.","type":"array","items":{"type":"string"}},"description":"Only these event types."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"subject":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"data":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"at":{"type":"string"}},"required":["id","type","subject","data","at"],"additionalProperties":false}},"cursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["events","cursor"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/events/wait":{"post":{"operationId":"wait_for_event","summary":"Wait for an event","description":"Block until an event of the given types happens (or timeout_seconds passes), then return it. Use instead of polling, e.g. wait for deploy.succeeded or approval.decided on a subject.","tags":["core"],"security":[{"bearer":[]},{"oauth2":["events:read"]}],"x-cranl-scope":"events:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"types":{"minItems":1,"type":"array","items":{"type":"string"}},"subject_id":{"description":"Only events about this app, database, approval… id.","type":"string"},"after":{"description":"Cursor from list_events; default: now.","type":"string"},"timeout_seconds":{"default":120,"type":"integer","minimum":1,"maximum":600}},"required":["types"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"event":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string"},"subject":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"data":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"at":{"type":"string"}},"required":["id","type","subject","data","at"],"additionalProperties":false},{"type":"null"}]},"timed_out":{"type":"boolean"},"cursor":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["event","timed_out","cursor"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks":{"post":{"operationId":"create_webhook","summary":"Create webhook","description":"POST every matching event to a URL (https only, a public address; redirects are not followed), signed with a secret shown once: header CranL-Signature: t=<unix>,v1=<hex HMAC-SHA256 of \"t.body\">. events: exact types, \"deploy.*\"-style groups, or [\"*\"].","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:write"]}],"x-cranl-scope":"events:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"default":["*"],"type":"array","items":{"type":"string"}},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["url"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"secret":{"type":"string"},"note":{"type":"string"}},"required":["id","url","events","secret","note"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"list_webhooks","summary":"List webhooks","description":"Webhooks in this account with their last delivery status. Webhooks switch off after 20 failed deliveries in a row.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:read"]}],"x-cranl-scope":"events:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"webhooks":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["webhooks"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/webhooks/{webhook_id}":{"delete":{"operationId":"delete_webhook","summary":"Delete webhook","description":"Stop sending events to a webhook.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:write"]}],"x-cranl-scope":"events:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Webhook id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/monitors":{"post":{"operationId":"create_monitor","summary":"Create uptime monitor","description":"Check an https URL on an interval (default: the app's URL every minute). The URL, and anything it redirects to, must be a public https address; a redirect anywhere else counts as a failure. Two failures in a row fire app.down (people are notified too); recovery fires app.up. Pair with create_webhook or wait_for_event.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:write"]}],"x-cranl-scope":"events:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"app_id":{"type":"string"},"url":{"description":"Default: the app's URL (+ path).","type":"string","format":"uri"},"path":{"description":"Appended to the app's URL, e.g. /health.","type":"string"},"name":{"type":"string","maxLength":100},"interval_seconds":{"default":60,"type":"integer","minimum":30,"maximum":3600},"expected_status":{"description":"Default: any status below 400.","type":"integer","minimum":100,"maximum":599},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"interval_seconds":{"type":"number"}},"required":["id","url","interval_seconds"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"list_monitors","summary":"List uptime monitors","description":"Monitors with their current status (up, down, unknown), last latency and last error.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:read"]}],"x-cranl-scope":"events:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"monitors":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["monitors"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/monitors/{monitor_id}":{"delete":{"operationId":"delete_monitor","summary":"Delete uptime monitor","description":"Stop checking a URL.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:write"]}],"x-cranl-scope":"events:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"monitor_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Monitor id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/settings":{"patch":{"operationId":"configure_app","summary":"Configure app safety features","description":"Turn on automatic rollback (a deploy that stops answering within 10 minutes is rolled back to the last good version), automatic preview apps for GitHub pull requests, or protection (agents can no longer delete or reconfigure it; only a person in the dashboard can undo that).","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"auto_rollback":{"type":"boolean"},"pr_previews":{"description":"GitHub apps only: a preview app per pull request, with the link commented on the PR.","type":"boolean"},"protect":{"description":"Protect the app. Removing protection is dashboard-only. Not available on an unclaimed temporary account.","type":"boolean","const":true},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app_id":{"type":"string"},"auto_rollback":{"type":"boolean"},"pr_previews":{"type":"boolean"},"protected":{"type":"boolean"}},"required":["app_id","auto_rollback","pr_previews","protected"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/protect":{"post":{"operationId":"protect_database","summary":"Protect a database","description":"Agents can no longer delete, stop or restore snapshots into this database. Removing protection is dashboard-only. Not available on an unclaimed temporary account.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"database_id":{"type":"string"},"protected":{"type":"boolean","const":true}},"required":["database_id","protected"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/approvals/{approval_id}":{"get":{"operationId":"get_approval","summary":"Get approval","description":"State of an approval requested with request_approval: pending, approved (with the confirm_token to pass when repeating the call), denied or expired.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["account:read"]}],"x-cranl-scope":"account:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"approval_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Approval id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/approvals":{"get":{"operationId":"list_approvals","summary":"List approvals","description":"Approval requests in this account, newest first.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["account:read"]}],"x-cranl-scope":"account:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["pending","approved","denied","expired","used"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"approvals":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["approvals"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/undo":{"post":{"operationId":"undo_action","summary":"Undo an action","description":"Reverse a recent action: an environment change, a deleted app or database, a new domain, cron job, webhook, monitor, preview, a deploy (rolls back) or something just created (deletes it). Without activity_id, undoes this credential's latest undoable action. Destructive: needs a confirmation token.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["account:read"]}],"x-cranl-scope":"account:read","x-cranl-annotations":{"destructive":true},"parameters":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"activity_id":{"description":"From list_activity; default: the latest undoable action of this key.","type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"undone":{"type":"string"},"result":{}},"required":["undone","result"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/estimate":{"post":{"operationId":"estimate_cost","summary":"Estimate cost","description":"What creating something would cost on this account's plan before creating it: whether it fits the plan, the monthly price for paid add-ons (sandboxes), and the plan to upgrade to when it does not fit.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["account:read"]}],"x-cranl-scope":"account:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"kind":{"type":"string","enum":["app","database","static_site","preview","sandbox","bucket","cdn_zone","backend","custom_domain"]},"count":{"default":1,"type":"integer","minimum":1,"maximum":50},"vcpu":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"ram_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"storage_gb":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["kind"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/preflight":{"post":{"operationId":"preflight_deploy","summary":"Check before deploying","description":"Read the source you are about to deploy and predict failures before a build starts: framework, start command, the port the app listens on vs the one configured, environment variables the code reads that are not set (pass app_id to compare with the app's current variables), .env or key files included by mistake. Same archive as deploy_source; nothing is deployed.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"archive_base64":{"type":"string"},"upload_id":{"type":"string"},"app_id":{"description":"An existing app to compare port and environment against.","type":"string"},"kind":{"type":"string","enum":["app","static"]},"port":{"description":"The port you intend to configure (new apps).","type":"integer","minimum":1,"maximum":65535}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/fix-pr":{"post":{"operationId":"create_fix_pr","summary":"Open a fix pull request","description":"For an app built from GitHub whose deploy failed: take CranL's AI-suggested fix for that deployment and open it as a pull request on the repository (a new branch off the app's branch). Merging it redeploys. Environment-variable fixes are not part of the PR; apply those with set_env.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"deployment_id":{"description":"Default: the latest failed deployment.","type":"string"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/snapshots":{"post":{"operationId":"snapshot_database","summary":"Snapshot a database","description":"Save every table's rows so they can be restored with restore_database_snapshot. On PostgreSQL only the tables of the public schema are saved; the result's other_schemas names the schemas whose tables were left out. Kept 7 days. PostgreSQL, MySQL and MariaDB. The result's durable field says whether the snapshot is in storage that survives a server restart; when it is false, do not treat it as the only copy.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":200},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"operationId":"list_database_snapshots","summary":"List database snapshots","description":"Snapshots of a database, newest first, with their tables and row counts, and whether each is in durable storage. On PostgreSQL a snapshot holds the tables of the public schema only; other_schemas names the schemas whose tables it left out.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:read"]}],"x-cranl-scope":"db:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"snapshots":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["snapshots"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/snapshots/{snapshot_id}/restore":{"post":{"operationId":"restore_database_snapshot","summary":"Restore a database snapshot","description":"Replace table data with a snapshot's rows (all tables, or the ones named). Rows added since the snapshot are lost. On PostgreSQL only tables of the public schema are snapshotted and restored: tables in other schemas are left as they are, and the result's other_schemas names those schemas. Tables whose columns changed are skipped and reported. Only the tables being restored are emptied: if a table that is not being restored has a foreign key into one that is, nothing is restored and the error names it (on MySQL/MariaDB only tables the database's account can see are checked). Restoring a partitioned table replaces the rows of all its current partitions; a table with rows of its own in the snapshot that has since become one of them is listed in replaced_partitions (its snapshot rows are not put back), and if it was asked for as well, nothing is restored. On PostgreSQL the restored tables' triggers and INSERT rules are switched off while their rows go back. Triggers that cannot be switched off (every INSERT and DELETE trigger on MySQL/MariaDB; on PostgreSQL those of tables the database's account does not own) would run, so the restore is refused unless accept_triggers is passed, and triggers_fired then names them. On MySQL/MariaDB, tables on a storage engine without transactions (MyISAM, Aria, MEMORY) are refused unless accept_non_transactional_tables is passed. A fresh snapshot of the current data is taken first. Destructive: needs a confirmation token.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}},{"name":"snapshot_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Snapshot id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"tables":{"type":"array","items":{"type":"string"}},"accept_non_durable_snapshot":{"description":"MySQL/MariaDB only. Restore even though the snapshot of the current data taken first, the only way back, would be kept in temporary storage that a server restart can wipe. Without it such a restore is refused.","type":"boolean"},"accept_triggers":{"description":"Restore even though triggers on the restored tables that cannot be switched off will run on the rows going back, which can change those rows and write to other tables (MySQL/MariaDB: every INSERT and DELETE trigger; PostgreSQL: those of tables the database's account does not own). Without it such a restore is refused and the error names them.","type":"boolean"},"accept_non_transactional_tables":{"description":"MySQL/MariaDB only. Restore tables on a storage engine without transactions (MyISAM, Aria, MEMORY). If the restore fails part way, those tables stay emptied or partly refilled instead of being rolled back. Without it such a restore is refused.","type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/migrations":{"post":{"operationId":"run_migration","summary":"Run a database migration safely","description":"Run SQL (DDL and data changes) against a database. PostgreSQL: all statements run in one transaction, so a failing statement leaves the database untouched; dry_run: true applies it and rolls back to show what would happen, except for what PostgreSQL does not roll back: a sequence moved by setval or nextval (including by INSERTs into serial and identity columns) stays moved, and anything done outside this transaction, such as dblink calls or replication slots, stays done. MySQL/MariaDB (DDL is not transactional there): a snapshot is taken first and statements stop at the first error; restore_database_snapshot undoes the data; dry_run is not available and is refused. The script must not manage the transaction itself: BEGIN and COMMIT are ignored, while ROLLBACK, SAVEPOINT, PREPARE TRANSACTION and the like are refused. Returns per-statement results.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"sql":{"type":"string","minLength":1,"maxLength":2000000},"dry_run":{"type":"boolean"},"snapshot":{"description":"Take a snapshot first (default: true for MySQL/MariaDB, false for PostgreSQL). On PostgreSQL it holds the tables of the public schema only; the result's snapshot_other_schemas names the schemas left out.","type":"boolean"},"accept_non_durable_snapshot":{"description":"MySQL/MariaDB only. Run even though the snapshot, the only undo there, would be kept in temporary storage that a server restart can wipe. Without it such a migration is refused.","type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["sql"],"additionalProperties":false}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/import":{"post":{"operationId":"import_sql","summary":"Import SQL into a database","description":"Load a SQL script (a dump made with INSERT statements, e.g. pg_dump --inserts or mysqldump) from sql or an https sql_url (a public address, at most 50 MB; redirects are followed only to other public https addresses). Runs like run_migration: one transaction on PostgreSQL, snapshot first on MySQL/MariaDB. There is no dry run: an import that succeeds is kept. COPY … FROM stdin blocks are not supported. When a statement fails, failed.index is its position in the script, counting from 0. For sql_url the statement's text is not returned, and the database's error comes without the part it quotes; its code (and, on PostgreSQL, the character position in the statement) point to the problem.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"sql":{"type":"string","maxLength":20000000},"sql_url":{"type":"string","format":"uri"},"accept_non_durable_snapshot":{"description":"MySQL/MariaDB only. Import even though the snapshot, the only undo there, would be kept in temporary storage that a server restart can wipe. Without it such an import is refused.","type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[],"additionalProperties":false}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/snapshots/{snapshot_id}":{"delete":{"operationId":"delete_database_snapshot","summary":"Delete a database snapshot","description":"Delete a snapshot before it expires.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}},{"name":"snapshot_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Snapshot id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/backups/policy":{"get":{"operationId":"get_backup_policy","summary":"Backup schedule","description":"A database's backup schedule and state: when it runs (in its time zone), how many backups are kept (1–30), whether it is paused and why, the next run, the last success and whether the latest scheduled backup failed. Redis is not backed up.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:read"]}],"x-cranl-scope":"db:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"supported":{"type":"boolean"},"enabled":{"type":"boolean"},"frequency":{"type":"string","enum":["hourly","every_6_hours","every_12_hours","daily","weekly"]},"hour":{"type":"number"},"minute":{"type":"number"},"weekday":{"type":"number"},"timezone":{"type":"string"},"keep":{"type":"number"},"max_keep":{"type":"number"},"manual_keep":{"type":"number"},"paused":{"anyOf":[{"type":"string","enum":["off","stopped","deleting","locked"]},{"type":"null"}]},"next_run_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"last_success_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"last_failure_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"failing":{"type":"boolean"},"late":{"type":"boolean"},"kept_count":{"type":"number"},"kept_bytes":{"type":"number"},"setup_error":{"anyOf":[{"type":"string"},{"type":"null"}]},"your_role":{"type":"string","enum":["view","operate","manage"]},"customized":{"type":"boolean"}},"required":["supported","enabled","frequency","hour","minute","weekday","timezone","keep","max_keep","manual_keep","paused","next_run_at","last_success_at","last_failure_at","failing","late","kept_count","kept_bytes","setup_error","your_role","customized"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"operationId":"update_backup_policy","summary":"Change the backup schedule","description":"Turn scheduled backups on or off, or change when they run and how many are kept (1–30). frequency: hourly, every_6_hours, every_12_hours, daily or weekly; hour and minute are local to timezone (an IANA name such as Asia/Riyadh); weekday (0 = Sunday) is for weekly; for every 6 or 12 hours, hour is the first run of the day. Takes effect at once.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"frequency":{"type":"string","enum":["hourly","every_6_hours","every_12_hours","daily","weekly"]},"hour":{"type":"integer","minimum":0,"maximum":23},"minute":{"type":"integer","minimum":0,"maximum":59},"weekday":{"type":"integer","minimum":0,"maximum":6},"timezone":{"type":"string","maxLength":64},"keep":{"type":"integer","minimum":1,"maximum":30},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"supported":{"type":"boolean"},"enabled":{"type":"boolean"},"frequency":{"type":"string","enum":["hourly","every_6_hours","every_12_hours","daily","weekly"]},"hour":{"type":"number"},"minute":{"type":"number"},"weekday":{"type":"number"},"timezone":{"type":"string"},"keep":{"type":"number"},"max_keep":{"type":"number"},"manual_keep":{"type":"number"},"paused":{"anyOf":[{"type":"string","enum":["off","stopped","deleting","locked"]},{"type":"null"}]},"next_run_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"last_success_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"last_failure_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"failing":{"type":"boolean"},"late":{"type":"boolean"},"kept_count":{"type":"number"},"kept_bytes":{"type":"number"},"setup_error":{"anyOf":[{"type":"string"},{"type":"null"}]},"your_role":{"type":"string","enum":["view","operate","manage"]},"customized":{"type":"boolean"}},"required":["supported","enabled","frequency","hour","minute","weekday","timezone","keep","max_keep","manual_keep","paused","next_run_at","last_success_at","last_failure_at","failing","late","kept_count","kept_bytes","setup_error","your_role","customized"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/backups":{"get":{"operationId":"list_backups","summary":"List backups","description":"A database's backups, newest first: scheduled ones, ones taken by hand (manual) and the safety copies taken before a restore (safety), with status (running, succeeded, failed), size and time. Also any scheduled backup running now and the latest restores.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:read"]}],"x-cranl-scope":"db:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"backups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string"},"size_bytes":{"anyOf":[{"type":"number"},{"type":"null"}]},"started_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"error":{"anyOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"anyOf":[{"type":"string"},{"type":"null"}]},"file_name":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","size_bytes","started_at","finished_at","error","triggered_by","file_name"],"additionalProperties":false}},"running_scheduled":{"anyOf":[{"type":"object","properties":{"started_at":{"type":"string"}},"required":["started_at"],"additionalProperties":false},{"type":"null"}]},"restores":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["backups","running_scheduled","restores"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_backup","summary":"Back up now","description":"Take a backup of a database now. Returns at once with the backup running; pass wait: true to answer when it finished. Backups taken by hand are kept apart from the schedule (the newest 10 stay). Not for Redis.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"wait":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"backup":{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string"},"size_bytes":{"anyOf":[{"type":"number"},{"type":"null"}]},"started_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"error":{"anyOf":[{"type":"string"},{"type":"null"}]},"triggered_by":{"anyOf":[{"type":"string"},{"type":"null"}]},"file_name":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","size_bytes","started_at","finished_at","error","triggered_by","file_name"],"additionalProperties":false}},"required":["backup"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/backups/{backup_id}/download":{"post":{"operationId":"get_backup_download_url","summary":"Download a backup","description":"A link that downloads a backup file for five minutes: a gzip of the engine's own dump (pg_dump custom format for PostgreSQL, SQL for MySQL and MariaDB, a mongodump archive for MongoDB). Owners and admins only.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:read"]}],"x-cranl-scope":"db:read","x-cranl-annotations":{"readOnly":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}},{"name":"backup_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Backup id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"file_name":{"type":"string"},"expires_at":{"type":"string"}},"required":["url","file_name","expires_at"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/backups/{backup_id}/restore":{"post":{"operationId":"restore_backup","summary":"Restore a backup","description":"Restore a backup. into: \"new\" (the default) makes a new database beside this one, same engine, region and project, and restores into it, leaving this one untouched; point apps at it once it looks right. into: \"replace\" overwrites this database's data, after taking a safety backup of it (if that fails, nothing changes). Runs in the background: follow it with get_restore. Destructive: needs a confirmation token. Owners and admins only.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}},{"name":"backup_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Backup id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"into":{"type":"string","enum":["new","replace"]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"restore":{"type":"object","properties":{"id":{"type":"string"},"mode":{"type":"string","enum":["replace","new"]},"status":{"type":"string","enum":["running","succeeded","failed"]},"step":{"type":"string"},"target_database_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"safety_backup_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"error":{"anyOf":[{"type":"string"},{"type":"null"}]},"log":{"type":"array","items":{"type":"string"}},"started_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","mode","status","step","target_database_id","safety_backup_id","error","log","started_at","finished_at"],"additionalProperties":false}},"required":["restore"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/restores/{restore_id}":{"get":{"operationId":"get_restore","summary":"Restore progress","description":"How a restore is going: its step (safety, creating, restoring, finished), status and log, and the new database's id for into: new.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:read"]}],"x-cranl-scope":"db:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}},{"name":"restore_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Restore id"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"restore":{"type":"object","properties":{"id":{"type":"string"},"mode":{"type":"string","enum":["replace","new"]},"status":{"type":"string","enum":["running","succeeded","failed"]},"step":{"type":"string"},"target_database_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"safety_backup_id":{"anyOf":[{"type":"string"},{"type":"null"}]},"error":{"anyOf":[{"type":"string"},{"type":"null"}]},"log":{"type":"array","items":{"type":"string"}},"started_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","mode","status","step","target_database_id","safety_backup_id","error","log","started_at","finished_at"],"additionalProperties":false}},"required":["restore"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/backups/{backup_id}":{"delete":{"operationId":"delete_backup","summary":"Delete a backup","description":"Delete one backup file for good. Destructive: needs a confirmation token. Owners and admins only.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"destructive":true,"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}},{"name":"backup_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Backup id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/source":{"put":{"operationId":"update_app_source","summary":"Change an app's repository or branch","description":"Point a GitHub-built app at another repository, branch or build folder (build_path, for monorepos). The repository must be one the account's GitHub connection can reach (list_github_repos). deploy: true builds the new source right away.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"repository":{"type":"string","minLength":1,"description":"\"owner/name\" or a repository id from list_github_repos."},"branch":{"type":"string"},"build_path":{"description":"Folder to build from, e.g. \"apps/web\". Default: the repository root.","type":"string"},"deploy":{"type":"boolean"},"wait":{"description":"With deploy: true, return when the deploy finishes.","type":"boolean"},"timeout_seconds":{"type":"integer","minimum":10,"maximum":900},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["repository"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}},"operation":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false},{"type":"null"}]}},"required":["app","operation"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/image":{"patch":{"operationId":"update_docker_image","summary":"Change a Docker app's image","description":"Change the image a Docker app runs (e.g. ghcr.io/me/api:v2) and/or its registry login, then redeploy (deploy: false to only save). remove_credentials: true drops a saved login.","tags":["apps"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"image":{"type":"string"},"registry_username":{"type":"string"},"registry_password":{"type":"string"},"remove_credentials":{"type":"boolean"},"deploy":{"description":"Default true.","type":"boolean"},"wait":{"description":"With deploy: true, return when the deploy finishes.","type":"boolean"},"timeout_seconds":{"type":"integer","minimum":10,"maximum":900},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"app":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"kind":{"type":"string","enum":["web","worker"]},"url":{"anyOf":[{"type":"string"},{"type":"null"}]},"region":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","name","status","url","region"],"additionalProperties":{}},"operation":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string","enum":["running","succeeded","failed","canceled"]},"stage":{"anyOf":[{"type":"string"},{"type":"null"}]},"target":{"type":"object","properties":{"type":{"anyOf":[{"type":"string"},{"type":"null"}]},"id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["type","id"],"additionalProperties":false},"result":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"error":{"anyOf":[{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"null"}]},"created_at":{"type":"string"},"updated_at":{"type":"string"},"finished_at":{"anyOf":[{"type":"string"},{"type":"null"}]},"deployment_id":{"anyOf":[{"type":"string"},{"type":"null"}]}},"required":["id","kind","status","stage","target","result","error","created_at","updated_at","finished_at"],"additionalProperties":false},{"type":"null"}]}},"required":["app","operation"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/edge-rules":{"get":{"operationId":"list_edge_rules","summary":"List redirect, header and cache rules","description":"Rules applied at the CDN in front of an app (app_id) or a CDN zone (cdn_zone_id): redirects, headers, blocking, cache times.","tags":["domains"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"query","required":false,"schema":{"description":"The app whose CDN the rule applies to.","type":"string"},"description":"The app whose CDN the rule applies to."},{"name":"cdn_zone_id","in":"query","required":false,"schema":{"description":"Or a standalone CDN zone.","type":"string"},"description":"Or a standalone CDN zone."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"rules":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["rules"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"set_edge_rule","summary":"Add or change a redirect, header or cache rule","description":"Add a rule at the CDN in front of an app or CDN zone, or change one (rule_id). when.match decides what is compared (url, request_header, response_header, extension, country, ip, query_string, status_code) against when.patterns (wildcards allowed, e.g. \"*/old/*\"); a header match also needs when.header. action is one of force_https, redirect, change_origin, cache_seconds, block, set_response_header, set_request_header, force_download, browser_cache_seconds, ignore_cache_control, disable_cors: redirect / change_origin need value (a URL), cache_seconds / browser_cache_seconds need value (seconds), set_*_header need header_name and header_value. Examples: redirect *.example.com/blog/* to https://example.com/news; block country RU; set_response_header X-Frame-Options DENY on url *.","tags":["domains"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"app_id":{"description":"The app whose CDN the rule applies to.","type":"string"},"cdn_zone_id":{"description":"Or a standalone CDN zone.","type":"string"},"rule_id":{"description":"Change this rule instead of adding one.","type":"string"},"when":{"type":"object","properties":{"match":{"type":"string","enum":["url","request_header","response_header","extension","country","ip","query_string","status_code"]},"patterns":{"minItems":1,"maxItems":20,"type":"array","items":{"type":"string","minLength":1}},"header":{"type":"string"}},"required":["match","patterns"]},"action":{"type":"string","enum":["force_https","redirect","change_origin","cache_seconds","block","set_response_header","set_request_header","force_download","browser_cache_seconds","ignore_cache_control","disable_cors"]},"value":{"type":"string"},"header_name":{"type":"string"},"header_value":{"type":"string"},"description":{"type":"string","maxLength":255},"enabled":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["when","action"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"rules":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["rules"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/edge-rules/{rule_id}":{"delete":{"operationId":"delete_edge_rule","summary":"Delete a redirect, header or cache rule","description":"Remove one rule (rule_id from list_edge_rules) from an app's CDN or a CDN zone.","tags":["domains"],"security":[{"bearer":[]},{"oauth2":["apps:write"]}],"x-cranl-scope":"apps:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"rule_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Rule id"}},{"name":"app_id","in":"query","required":false,"schema":{"description":"The app whose CDN the rule applies to.","type":"string"},"description":"The app whose CDN the rule applies to."},{"name":"cdn_zone_id","in":"query","required":false,"schema":{"description":"Or a standalone CDN zone.","type":"string"},"description":"Or a standalone CDN zone."},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/databases/{database_id}/external-access":{"put":{"operationId":"set_database_external_access","summary":"Turn a database's external access on or off","description":"External access gives the database a public port so tools outside CranL can connect (your laptop, a BI tool). snapshot_database, run_migration and import_sql need it. Turning it off closes the port; apps on CranL keep connecting internally either way.","tags":["databases"],"security":[{"bearer":[]},{"oauth2":["db:write"]}],"x-cranl-scope":"db:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"database_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Database id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["enabled"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"database":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"external_port":{"anyOf":[{"type":"number"},{"type":"null"}]}},"required":["database","external_port"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/team":{"get":{"operationId":"list_team","summary":"List teammates","description":"Who can reach this account: co-owners (full control) and pending invitations; with project_id, that project's members (admin or viewer) too.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["team:read"]}],"x-cranl-scope":"team:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"project_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/projects/{project_id}/members":{"post":{"operationId":"invite_to_project","summary":"Invite someone to a project","description":"Email an invitation to join one project as \"admin\" (can deploy and change settings) or \"viewer\" (read only). They get access to that project only.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["team:write"]}],"x-cranl-scope":"team:write","x-cranl-annotations":{"openWorld":true},"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Project id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"role":{"default":"viewer","type":"string","enum":["admin","viewer"]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["email"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/team/co-owners":{"post":{"operationId":"invite_co_owner","summary":"Invite a co-owner","description":"Email an invitation giving someone full control of this whole account (every project, billing, deleting things). Needs a full-access credential and the user's confirmation. For access to one project, use invite_to_project.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["team:write"]}],"x-cranl-scope":"team:write","x-cranl-annotations":{"destructive":true,"openWorld":true},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["email"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/team/members":{"delete":{"operationId":"remove_teammate","summary":"Remove a teammate","description":"Take away someone's access: a co-owner (member_id from list_team co_owners), a project member (with project_id), or a pending invitation (invitation_id). The account owner cannot be removed.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["team:write"]}],"x-cranl-scope":"team:write","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"member_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"project_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"invitation_id","in":"query","required":false,"schema":{"type":"string"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/alert-channels":{"get":{"operationId":"list_alert_channels","summary":"List alert channels","description":"Where deploy results and alerts are sent: Slack, Discord, Telegram, Microsoft Teams or webhooks.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:read"]}],"x-cranl-scope":"events:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"channels":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["channels"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"add_alert_channel","summary":"Add an alert channel","description":"Send deploy results, downtime and approval requests to a chat or webhook. slack / discord / teams / webhook need url (the incoming-webhook URL); telegram needs bot_token and chat_id. on_success / on_failure choose which deploy results are sent (default both). Send a test with test_alert_channel.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:write"]}],"x-cranl-scope":"events:write","x-cranl-annotations":{"openWorld":true},"parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["slack","discord","telegram","teams","webhook"]},"name":{"type":"string","minLength":1,"maxLength":255},"url":{"type":"string"},"bot_token":{"type":"string"},"chat_id":{"type":"string"},"on_success":{"type":"boolean"},"on_failure":{"type":"boolean"},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["type","name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/alert-channels/{channel_id}/test":{"post":{"operationId":"test_alert_channel","summary":"Send a test alert","description":"Send a test message to a saved alert channel, to check it arrives.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:write"]}],"x-cranl-scope":"events:write","x-cranl-annotations":{"openWorld":true,"idempotent":true},"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Alert channel id"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":[]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/alert-channels/{channel_id}":{"delete":{"operationId":"delete_alert_channel","summary":"Delete an alert channel","description":"Stop sending alerts to a channel and forget its settings.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:write"]}],"x-cranl-scope":"events:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"channel_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Alert channel id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/api-keys":{"get":{"operationId":"list_api_keys","summary":"List API keys","description":"This user's active API keys: name, access, limits, expiry and last use. The secrets themselves are never shown again.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["account:read"]}],"x-cranl-scope":"account:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["keys"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"create_api_key","summary":"Create an API key","description":"Create a key, e.g. for CI or another agent. Give it only what it needs: a preset (full, deploy, readonly, builder) or explicit scopes, optionally limited to named apps and databases (apps / databases), with an expiry (30, 90 or 365 days; default 90). A key can never be given access the calling credential does not have, and an unknown scope name is refused. A key created by an API key expires no later than that key, stops working when it is revoked, and shares its spending cap: the cap covers that key and every key made from it, and revoking one gives nothing back. A key created through a connected app (OAuth) stops working when the app is disconnected or its access is revoked, and so do the keys made from it. A key limited to named apps or databases cannot create keys. The secret is returned once.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["keys:write"]}],"x-cranl-scope":"keys:write","x-cranl-annotations":{},"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Retrying with the same key returns the first result instead of acting twice."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":255},"preset":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"apps":{"description":"Limit the key to these app ids.","type":"array","items":{"type":"string"}},"databases":{"description":"Limit the key to these database ids.","type":"array","items":{"type":"string"}},"expires_in_days":{"anyOf":[{"type":"number","const":30},{"type":"number","const":90},{"type":"number","const":365}]},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"}},"required":["key"],"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/api-keys/{key_id}":{"delete":{"operationId":"revoke_api_key","summary":"Revoke an API key","description":"Revoke one of this user's keys (key_id from list_api_keys), and any keys it created. Anything using them stops working within 30 seconds.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["keys:write"]}],"x-cranl-scope":"keys:write","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"API key id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/upgrade-link":{"get":{"operationId":"get_upgrade_link","summary":"Get a link to change plan","description":"When a plan limit is in the way: the plans on offer and the link where the user picks one. Agents never pay or change the plan themselves; payment happens in the browser.","tags":["account"],"security":[{"bearer":[]},{"oauth2":["account:read"]}],"x-cranl-scope":"account:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"plan","in":"query","required":false,"schema":{"description":"The plan you suggest, from the list returned.","type":"string"},"description":"The plan you suggest, from the list returned."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string"},"current_plan":{"anyOf":[{"type":"string"},{"type":"null"}]},"plans":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["url","current_plan","plans"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/buckets/{bucket_id}/object":{"get":{"operationId":"download_object","summary":"Read a file from a bucket","description":"Read a file (up to 5 MB). Text files come back as text; others as content_base64.","tags":["storage"],"security":[{"bearer":[]},{"oauth2":["storage:read"]}],"x-cranl-scope":"storage:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"bucket_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Bucket id"}},{"name":"key","in":"query","required":true,"schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"key":{"type":"string"},"size":{"type":"number"},"content_type":{"type":"string"},"text":{"type":"string"},"content_base64":{"type":"string"}},"required":["key","size","content_type"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_object","summary":"Delete a file from a bucket","description":"Delete one file, or a whole folder with folder: true (key \"images/\" and everything under it).","tags":["storage"],"security":[{"bearer":[]},{"oauth2":["storage:delete"]}],"x-cranl-scope":"storage:delete","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"bucket_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Bucket id"}},{"name":"key","in":"query","required":true,"schema":{"type":"string","minLength":1}},{"name":"folder","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/buckets/{bucket_id}":{"delete":{"operationId":"delete_bucket","summary":"Delete a bucket","description":"Delete a bucket and every file in it. Cannot be undone.","tags":["storage"],"security":[{"bearer":[]},{"oauth2":["storage:delete"]}],"x-cranl-scope":"storage:delete","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"bucket_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Bucket id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/cdn/zones/{zone_id}":{"delete":{"operationId":"delete_cdn_zone","summary":"Delete a CDN zone","description":"Delete a standalone CDN zone. Its hostname stops serving right away.","tags":["cdn"],"security":[{"bearer":[]},{"oauth2":["cdn:delete"]}],"x-cranl-scope":"cdn:delete","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"zone_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"CDN zone id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/email/domains/{domain_id}":{"delete":{"operationId":"delete_email_domain","summary":"Remove an email sending domain","description":"Stop sending from a domain (domain_id from list_email_domains). Mail sent from it afterwards is refused.","tags":["email"],"security":[{"bearer":[]},{"oauth2":["email:send"]}],"x-cranl-scope":"email:send","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"domain_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Email domain id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/projects/{project_id}":{"patch":{"operationId":"rename_project","summary":"Rename a project","description":"Give a project a new name.","tags":["core"],"security":[{"bearer":[]},{"oauth2":["projects:write"]}],"x-cranl-scope":"projects:write","x-cranl-annotations":{"idempotent":true},"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Project id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["name"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"previous_name":{"type":"string"}},"required":["id","name","previous_name"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"delete_project","summary":"Delete a project","description":"Delete an empty project. Delete or move its apps, databases, storage buckets and sandboxes first; a project that still has any is refused, including apps and databases that were deleted but are still in their 7-day recovery window (the error gives the date they go). Only the account's owner or an account admin may delete a project (forbidden otherwise).","tags":["core"],"security":[{"bearer":[]},{"oauth2":["projects:delete"]}],"x-cranl-scope":"projects:delete","x-cranl-annotations":{"destructive":true},"parameters":[{"name":"project_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Project id"}},{"name":"plan","in":"query","required":false,"schema":{"description":"Describe what would change without changing anything.","type":"boolean"},"description":"Describe what would change without changing anything."},{"name":"confirm","in":"query","required":false,"schema":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves."},{"name":"idempotency_key","in":"query","required":false,"schema":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"description":"Makes a retried request return the first result instead of acting twice."},{"name":"request_approval","in":"query","required":false,"schema":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"},"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/run":{"post":{"operationId":"run_command","summary":"Run a command in an app","description":"Run a one-off command inside the app's running container and get its output, e.g. database migrations (npx prisma migrate deploy, python manage.py migrate), a seed script, or a quick check (ls, env | cut -d= -f1). Runs with the app's environment variables. Not for long-running processes.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:exec"]}],"x-cranl-scope":"apps:exec","x-cranl-annotations":{},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"command":{"type":"string","minLength":1,"maxLength":4000},"shell":{"description":"Default sh.","type":"string","enum":["sh","bash"]},"timeout_seconds":{"description":"Default 120.","type":"integer","minimum":5,"maximum":600},"plan":{"description":"Describe what would change without changing anything.","type":"boolean"},"confirm":{"description":"Confirmation token for a destructive action. Call without it first; the error returns a token to use after the user approves.","type":"string"},"idempotency_key":{"description":"Makes a retried request return the first result instead of acting twice.","type":"string","maxLength":255},"request_approval":{"description":"For a destructive action when no one is in the chat: ask the account owner to approve it in the dashboard (they are emailed and pinged). Poll get_approval, then repeat the call with its confirm token.","type":"boolean"}},"required":["command"]}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["succeeded","failed","timed_out"]},"output":{"type":"string"},"truncated":{"type":"boolean"},"seconds":{"type":"number"}},"required":["status","output","truncated","seconds"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/logs/follow":{"get":{"operationId":"follow_app_logs","summary":"Follow an app's logs","description":"Wait for new runtime log lines, like tail -f: returns as soon as lines newer than after arrive (or when wait_seconds runs out), with a cursor to pass as after next time. Start without after to get the latest lines and a cursor. Narrow with grep and level.","tags":["observability"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"after","in":"query","required":false,"schema":{"description":"The cursor from the previous call.","type":"string"},"description":"The cursor from the previous call."},{"name":"wait_seconds","in":"query","required":false,"schema":{"description":"Default 30.","type":"integer","minimum":1,"maximum":120},"description":"Default 30."},{"name":"grep","in":"query","required":false,"schema":{"type":"string"}},{"name":"level","in":"query","required":false,"schema":{"type":"string","enum":["error","warn","info"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"lines":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"cursor":{"anyOf":[{"type":"string"},{"type":"null"}]},"timed_out":{"type":"boolean"}},"required":["lines","cursor","timed_out"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/apps/{app_id}/deployments/diff":{"get":{"operationId":"diff_deployments","summary":"What changed between two deploys","description":"Compare two deployments of an app, by default the latest one and the last successful one before it: the commits and files that changed in the code (GitHub apps), environment variables added, removed or changed (names only), and every change made to the app in between (who, what, when). The fastest way to find why a working app broke.","tags":["deployments"],"security":[{"bearer":[]},{"oauth2":["apps:read"]}],"x-cranl-scope":"apps:read","x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"app_id","in":"path","required":true,"schema":{"type":"string","minLength":1,"description":"Application id"}},{"name":"from","in":"query","required":false,"schema":{"description":"The older deployment id. Default: the last successful one before to.","type":"string"},"description":"The older deployment id. Default: the last successful one before to."},{"name":"to","in":"query","required":false,"schema":{"description":"The newer deployment id. Default: the latest.","type":"string"},"description":"The newer deployment id. Default: the latest."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/tools/search":{"get":{"operationId":"find_tools","summary":"Find the right tool","description":"Search every CranL operation by what you want to do (\"run a migration\", \"redirect www\", \"invite a teammate\", \"follow logs\") and get the best matches with their input schemas. Only operations this credential may call are returned. Over MCP, run one that is not in your tool list with call_operation.","tags":["core"],"x-cranl-scope":null,"x-cranl-annotations":{"readOnly":true,"idempotent":true},"parameters":[{"name":"query","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":200}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":20}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"tools":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["tools"],"additionalProperties":false}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/signup/start":{"post":{"operationId":"signup_start","summary":"Sign up or sign in by emailed code","description":"Sends a six-digit code to the address (twelve digits while many wrong codes are being tried). No credentials needed.","tags":["account"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"name":{"type":"string"}},"required":["email"]}}}},"responses":{"200":{"description":"Code sent; returns signup_id and code_length (6 or 12)"},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/signup/verify":{"post":{"operationId":"signup_verify","summary":"Finish an email-code sign-up","description":"Returns an API key (shown once). plan: free puts a new account on the Free plan.","tags":["account"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"signup_id":{"type":"string"},"code":{"type":"string"},"plan":{"type":"string","enum":["free","none"]},"account_name":{"type":"string"}},"required":["signup_id","code"]}}}},"responses":{"200":{"description":"api_key, user and account"},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/claimable":{"post":{"operationId":"create_claimable_account","summary":"Deploy without an account","description":"Returns a temporary key (deploy and read only, Free-plan limits) and a claim_url. Unclaimed resources are deleted after 72 hours.","tags":["account"],"responses":{"201":{"description":"api_key, claim_url and expires_at"},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/events/stream":{"get":{"operationId":"stream_events","summary":"Stream events (Server-Sent Events)","description":"Live events. Resume with the Last-Event-ID header or ?after=. The stream closes after ten minutes; reconnect.","tags":["events"],"security":[{"bearer":[]},{"oauth2":["events:read"]}],"parameters":[{"name":"types","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated event types."},{"name":"after","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"text/event-stream","content":{"text/event-stream":{"schema":{"type":"string"}}}},"default":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"field":{"type":"string"},"hint":{"type":"string"},"next_actions":{"type":"array","items":{"type":"object","required":["operation"],"properties":{"operation":{"type":"string"},"args":{"type":"object"},"why":{"type":"string"}}}},"docs_url":{"type":"string"},"details":{"type":"object"},"request_id":{"type":"string"}}}}}},"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","description":"An API key (cranl_sk_…) or OAuth access token (cranl_at_…)."},"oauth2":{"type":"oauth2","flows":{"authorizationCode":{"authorizationUrl":"https://app.cranl.com/oauth/authorize","tokenUrl":"https://app.cranl.com/api/oauth/token","refreshUrl":"https://app.cranl.com/api/oauth/token","scopes":{"account:read":"See the plan, limits, usage, regions and activity log.","projects:read":"List projects.","projects:write":"Create and rename projects.","projects:delete":"Delete empty projects.","apps:read":"See applications, deployments, logs, metrics, domains and requests.","apps:write":"Create and configure applications, previews, domains, cron jobs and volumes.","apps:deploy":"Deploy, restart, stop, start and roll back applications.","apps:delete":"Delete applications (recoverable for 7 days).","apps:exec":"Run commands inside running applications.","env:read":"Read environment variable names (values stay masked).","env:reveal":"Read environment variable values and database passwords, and use account secrets in sandboxes.","env:write":"Set and remove environment variables.","db:read":"See databases, their schema, and run read-only queries.","db:write":"Create databases, connect them to apps, and run queries that change data.","db:delete":"Delete databases (recoverable for 7 days).","email:read":"See email domains, templates and sending history.","email:send":"Send email from verified domains.","storage:read":"See buckets and objects.","storage:write":"Create buckets and access keys, and upload files.","storage:delete":"Delete buckets and files.","cdn:read":"See CDN zones.","cdn:write":"Create CDN zones, purge caches and edit edge rules.","cdn:delete":"Delete CDN zones.","sandboxes:read":"See sandboxes and read their files.","sandboxes:write":"Run commands in sandboxes, write files, expose ports, stop and restart them. Starting or waking one needs billing:spend as well.","agents:read":"See Cloud Agents runs, their timelines and diffs, environments and automations.","agents:write":"Start, message and stop Cloud Agents runs, and manage environments, automations, workspaces and model keys. Starting a run needs billing:spend as well.","realtime:read":"See Realtime apps, live channels, presence, recent messages and usage.","realtime:write":"Create and configure Realtime apps, publish messages, sign client tokens and end connections.","realtime:secrets":"Reveal and rotate Realtime secret keys, and connect them to apps.","realtime:delete":"Delete Realtime apps.","queues:read":"See queues, their messages, dead letters, schedules, inboxes, workers and usage.","queues:write":"Create and configure queues, subscriptions, schedules, inboxes and workers; send, receive and retry messages.","queues:secrets":"Create and revoke queue keys, reveal signing secrets, and connect queues to apps.","queues:delete":"Delete queues and subscriptions, and purge messages.","backends:read":"See managed backends.","backends:write":"Create and manage managed backends.","billing:spend":"Start anything that costs money. Payment always completes in the browser.","events:read":"See events, webhooks and monitors.","events:write":"Create webhooks, uptime monitors and alert channels.","team:read":"See teammates and pending invitations.","team:write":"Invite and remove teammates.","keys:write":"Create and revoke API keys (never with more access than the key doing it)."}}}}}}}