Skip to content

MCP Server

Let an AI assistant drive your account. 71 tools, one per API operation.

Manage API keys

Public beta

The MCP server is open in beta. Tools and responses may still change, and every call acts on your real deployments. Give an assistant a scoped key, never a full-access one.

What it is

MCP (Model Context Protocol) is the open standard for giving an AI assistant tools. Point a compatible client (Claude Desktop, Cursor, an in-app agent) at the endpoint below, and it can list, inspect and control your deployments in natural language: "restart my bot", "why did it crash", "show the last logs". Every tool maps to a REST API operation and runs through the exact same permissions, scopes and rate limits.

Connect

ChatGPT (OAuth)

Add a custom connector, paste the endpoint, choose OAuth. You sign in and approve the scopes once, then it stays connected:

  1. 1. Settings to Connectors to Add custom connector (MCP).
  2. 2. Server URL: https://bot-hosting.net/api/mcp
  3. 3. Authentication: OAuth (Dynamic Client Registration is auto-detected).
  4. 4. Create, then Connect to you're sent to Bot-Hosting to log in and approve the scopes.

Review or revoke connected apps anytime from your developer settings.

Claude Desktop / Cursor (API key)

Clients that don't do OAuth connect through the mcp-remote bridge with your bhk_ key as a Bearer token:

Client config
{
  "mcpServers": {
    "bot-hosting": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://bot-hosting.net/api/mcp",
        "--header",
        "Authorization: Bearer bhk_your_key"
      ]
    }
  }
}

Or test it directly with curl (calls a tool the same way the AI would):

curl https://bot-hosting.net/api/mcp \
  -H "Authorization: Bearer bhk_your_key" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"deployments_list","arguments":{}}}'

Auth & scopes

initialize and tools/list are open for discovery; tools/call requires the key, and the key's scopes gate what actually runs. Give an assistant a key with only the scopes it needs, e.g. deployments:read + deployments:power for a support bot that can restart but never delete.

Deployments

deployments:read List and inspect deployments
deployments:power Start, stop, restart, console
deployments:write Create, edit, delete, resize, move
projects:read List your projects
projects:write Create and delete projects
files:read Browse and download files
files:write Upload, edit, rename, delete files
env:read Read env vars (secrets stay masked)
env:write Set env vars
backups:read List backups
backups:write Create and delete backups
packages:read View installed packages
packages:write Add and remove packages

Account

account:read Read profile and quota
billing:read Read invoices and plan
credits:spend Spend credits to create resources

Templates

templates:read Browse anyone's public templates

Documentation

docs:read Search and read the Bot-Hosting docs

Tools

Deployments

deployments_sftp deployments:read

SFTP connection details for a deployment: host, port, username and the current password. The password is shown in full, so it needs BOTH deployments:read and deployments:write - a read-only credential cannot reveal it. Returns password: null if none is set yet (the owner generates one from the dashboard SFTP tab).

id
Returns
{
  "host": "string",
  "port": 25565,
  "username": "grality",
  "password": "string"
}
deployments_list deployments:read

List every deployment you can access (owned + shared). The `owned` flag tells them apart. Pass name to find one by name, and brief: true when you only need to pick one: it returns id, name, state, url, runtime and entry file instead of the full record.

name?brief?
Returns
{
  "deployments": [
    "string"
  ]
}
deployments_get deployments:read

Fetch a single deployment.

id
Returns
{
  "id": "dep_a1b2c3d4",
  "name": "my-bot",
  "description": "A cool Discord bot",
  "state": "running",
  "status": "active",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "projectId": "dep_a1b2c3d4",
  "owned": true,
  "owner": {
    "id": "dep_a1b2c3d4",
    "username": "grality"
  },
  "shared": [
    {
      "user": {
        "id": "dep_a1b2c3d4",
        "username": "grality"
      },
      "permissions": [
        "OVERVIEW"
      ]
    }
  ],
  "resources": {
    "ramMB": 512,
    "cpuPercent": 50,
    "storageMB": 1024
  },
  "runtime": "python",
  "entryFile": "string",
  "domains": {
    "subdomain": "my-bot.apps",
    "slug": "my-template",
    "custom": "bot.example.com",
    "url": "https://.../files/download?path=%2Fmain.py"
  },
  "node": {
    "name": "my-bot",
    "fqdn": "node-eu-1.bot-hosting.net",
    "region": "eu-west"
  },
  "port": 25565,
  "ports": [
    25565
  ],
  "startup": {
    "kind": "string",
    "runtime": "python",
    "runtimeVersion": "string",
    "entryFile": "string",
    "startCommand": "string",
    "engine": "string"
  },
  "connection": {
    "engine": "string",
    "host": "string",
    "port": 25565,
    "uriTemplate": "string"
  }
}
deployments_update deployments:write

Rename a deployment or change its description.

idname?description?
Returns
{
  "id": "dep_a1b2c3d4",
  "name": "my-bot",
  "description": "A cool Discord bot",
  "state": "running",
  "status": "active",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "projectId": "dep_a1b2c3d4",
  "owned": true,
  "owner": {
    "id": "dep_a1b2c3d4",
    "username": "grality"
  },
  "shared": [
    {
      "user": {
        "id": "dep_a1b2c3d4",
        "username": "grality"
      },
      "permissions": [
        "OVERVIEW"
      ]
    }
  ],
  "resources": {
    "ramMB": 512,
    "cpuPercent": 50,
    "storageMB": 1024
  },
  "runtime": "python",
  "entryFile": "string",
  "domains": {
    "subdomain": "my-bot.apps",
    "slug": "my-template",
    "custom": "bot.example.com",
    "url": "https://.../files/download?path=%2Fmain.py"
  },
  "node": {
    "name": "my-bot",
    "fqdn": "node-eu-1.bot-hosting.net",
    "region": "eu-west"
  },
  "port": 25565,
  "ports": [
    25565
  ]
}
deployments_move deployments:write

Move a deployment to another of your projects.

idtoProjectId
Returns
{
  "id": "dep_a1b2c3d4",
  "name": "my-bot",
  "description": "A cool Discord bot",
  "state": "running",
  "status": "active",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "projectId": "dep_a1b2c3d4",
  "owned": true,
  "owner": {
    "id": "dep_a1b2c3d4",
    "username": "grality"
  },
  "shared": [
    {
      "user": {
        "id": "dep_a1b2c3d4",
        "username": "grality"
      },
      "permissions": [
        "OVERVIEW"
      ]
    }
  ],
  "resources": {
    "ramMB": 512,
    "cpuPercent": 50,
    "storageMB": 1024
  },
  "runtime": "python",
  "entryFile": "string",
  "domains": {
    "subdomain": "my-bot.apps",
    "slug": "my-template",
    "custom": "bot.example.com",
    "url": "https://.../files/download?path=%2Fmain.py"
  },
  "node": {
    "name": "my-bot",
    "fqdn": "node-eu-1.bot-hosting.net",
    "region": "eu-west"
  },
  "port": 25565,
  "ports": [
    25565
  ]
}
deployments_delete deployments:write destructive

Delete a deployment (instant; teardown runs in the background).

id
Returns
{
  "ok": true
}
deployments_create deployments:write

Create a deployment (blank, from a public GitHub repo, or a template). Sized by default to an equal share of the free pool; the result tells you the RAM/CPU it got. A modern web stack needs 512+ MB to npm install (exit 137 = OOM): deployments.resize BEFORE the first start.

nameramMB?cpuPct?storageMB?description?projectId?sourcegithubRepo?branch?templateId?env?runtime?runtimeVersion?
Returns
{
  "deployment": {
    "id": "dep_a1b2c3d4",
    "name": "my-bot",
    "description": "A cool Discord bot",
    "state": "running",
    "status": "active",
    "createdAt": "2026-01-01T00:00:00.000Z",
    "projectId": "dep_a1b2c3d4",
    "owned": true,
    "owner": {
      "id": "dep_a1b2c3d4",
      "username": "grality"
    },
    "shared": [
      {
        "user": {
          "id": "dep_a1b2c3d4",
          "username": "grality"
        },
        "permissions": [
          "OVERVIEW"
        ]
      }
    ],
    "resources": {
      "ramMB": 512,
      "cpuPercent": 50,
      "storageMB": 1024
    },
    "runtime": "python",
    "entryFile": "string",
    "domains": {
      "subdomain": "my-bot.apps",
      "slug": "my-template",
      "custom": "bot.example.com",
      "url": "https://.../files/download?path=%2Fmain.py"
    },
    "node": {
      "name": "my-bot",
      "fqdn": "node-eu-1.bot-hosting.net",
      "region": "eu-west"
    },
    "port": 25565,
    "ports": [
      25565
    ],
    "startup": {
      "kind": "string",
      "runtime": "python",
      "runtimeVersion": "string",
      "entryFile": "string",
      "startCommand": "string",
      "engine": "string"
    },
    "connection": {
      "engine": "string",
      "host": "string",
      "port": 25565,
      "uriTemplate": "string"
    }
  },
  "next": "string"
}
deployments_getStartup deployments:read

Get the startup config: runtime, version, entry file, start command.

id
Returns
{
  "kind": "string",
  "runtime": "python",
  "runtimeVersion": "string",
  "entryFile": "string",
  "startCommand": "string",
  "engine": "string"
}
deployments_updateStartup deployments:write

Change runtime / version / entry file / start command (queues a rebuild). The start command is a FULL SHELL LINE run at every boot: it can install and build before starting - e.g. 'npm install && npm run build && exec node build/index.js'. There is no other way to run build steps, and there does not need to be: put them here and restart. Keep the install step: a command that only starts the app stops installing dependencies at boot.

idruntime?runtimeVersion?entryFile?startCommand?kind?engine?
Returns
{
  "kind": "string",
  "runtime": "python",
  "runtimeVersion": "string",
  "entryFile": "string",
  "startCommand": "string",
  "engine": "string",
  "warning": "string"
}
runtimes_list deployments:read

List available runtimes, services and database engines with their versions.

Returns
{
  "runtimes": [
    {
      "id": "dep_a1b2c3d4",
      "label": "Manual",
      "versions": [
        "string"
      ],
      "defaultVersion": "string",
      "defaultEntry": "string"
    }
  ],
  "services": [
    {
      "id": "dep_a1b2c3d4",
      "label": "Manual",
      "versions": [
        "string"
      ],
      "defaultVersion": "string",
      "defaultEntry": "string"
    }
  ],
  "databases": [
    {
      "id": "dep_a1b2c3d4",
      "label": "Manual",
      "versions": [
        "string"
      ],
      "defaultVersion": "string"
    }
  ]
}
deployments_getGit deployments:read

Get the linked GitHub source: repo, branch, auto-pull.

id
Returns
{
  "linked": true,
  "repo": "string",
  "branch": "string",
  "autoPull": true
}
deployments_setAutoPull deployments:write

Toggle auto-pull: re-pull the linked repo on every restart.

idautoPull
Returns
{
  "linked": true,
  "repo": "string",
  "branch": "string",
  "autoPull": true
}
deployments_power deployments:power

Send a power signal (start / stop / restart / kill). A start or a restart WAITS for the process to settle and hands back the state it reached, the console output it produced and what to do about it: ok:true only means the signal was accepted, the logs say whether the app actually came up. Never announce that something works on ok:true alone.

idactionwaitSeconds?
Returns
{
  "ok": true,
  "action": "restart",
  "reason": "string",
  "state": "running",
  "logs": [
    "string"
  ],
  "hint": "string"
}
deployments_logs deployments:read

Read the last N lines of the console log, cleaned of colour codes and progress spam. A build or install takes MINUTES: pass waitSeconds (e.g. 25) to wait server-side before reading, instead of calling this again and again - polling in a loop is how a whole conversation gets eaten by npm output. Returns the live state alongside.

idsize?waitSeconds?
Returns
{
  "lines": [
    "Bot is online!"
  ],
  "state": "running",
  "settled": true,
  "hint": "string"
}
deployments_searchLogs deployments:read

Find lines in the console log that deployments.logs does not show: it searches the last 2000 lines (up to 5000) for a word or a regular expression, case-insensitive, and returns each hit with a few lines around it. Use it when the tail looks fine but something went wrong earlier, or to find the first occurrence of an error.

idpatternlines?context?
Returns
{
  "matches": [
    {
      "line": 1,
      "text": "string",
      "around": [
        "string"
      ]
    }
  ],
  "scanned": 1,
  "truncated": true
}
deployments_resources deployments:read

Live resource usage: CPU, memory, disk, network and uptime.

id
Returns
{
  "state": "running",
  "cpu": {
    "usedPercent": 50,
    "limitPercent": 50
  },
  "memory": {
    "usedBytes": 1048576,
    "limitBytes": 1048576
  },
  "disk": {
    "usedBytes": 1048576,
    "limitBytes": 1048576
  },
  "network": {
    "rxBytes": 1048576,
    "txBytes": 1048576
  },
  "uptimeMs": 3600000
}
deployments_command deployments:power

Write a line to the running process's stdin (a game server console, a REPL). NOT a shell and there is no shell: a web server ignores stdin, so curl / node / npm typed here do nothing. No output is returned. To send an HTTP request to the app use deployments.check; to run a script, put it in the start command.

idcommand
Returns
{
  "ok": true,
  "note": "string"
}
deployments_resize deployments:write

Change RAM / CPU / storage allocation (drawn from your plan pool).

idramMBcpuPctstorageMB
Returns
{
  "id": "dep_a1b2c3d4",
  "name": "my-bot",
  "description": "A cool Discord bot",
  "state": "running",
  "status": "active",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "projectId": "dep_a1b2c3d4",
  "owned": true,
  "owner": {
    "id": "dep_a1b2c3d4",
    "username": "grality"
  },
  "shared": [
    {
      "user": {
        "id": "dep_a1b2c3d4",
        "username": "grality"
      },
      "permissions": [
        "OVERVIEW"
      ]
    }
  ],
  "resources": {
    "ramMB": 512,
    "cpuPercent": 50,
    "storageMB": 1024
  },
  "runtime": "python",
  "entryFile": "string",
  "domains": {
    "subdomain": "my-bot.apps",
    "slug": "my-template",
    "custom": "bot.example.com",
    "url": "https://.../files/download?path=%2Fmain.py"
  },
  "node": {
    "name": "my-bot",
    "fqdn": "node-eu-1.bot-hosting.net",
    "region": "eu-west"
  },
  "port": 25565,
  "ports": [
    25565
  ]
}
deployments_sync deployments:write

Pull the latest code from the linked GitHub repo (restarts it if it was running). Pass branch, and repo if it changed, to FIX a wrong branch after a failed clone: that repoints the deployment and pulls, and is the way to recover, never deleting and recreating.

idrepo?branch?
Returns
{
  "ok": true,
  "commit": "string",
  "repo": "string",
  "branch": "string"
}
deployments_diagnose deployments:read

Everything needed to answer "what is wrong with this deployment", in ONE call: live state, RAM and CPU, the cleaned log tail, the startup config, and what the public URL returns. Use this INSTEAD of the deployments.get + deployments.logs + deployments.check + deployments.getStartup sequence - it replaces all four and it reads them together, so it can say things none of them can alone (a healthy log next to a 500 page, or an app port that is silent only because the install is still running). Start every investigation here.

idwaitSeconds?
Returns
{
  "state": "running",
  "installing": true,
  "resources": {
    "ramMB": 512,
    "ramLimitMB": 512,
    "cpuPercent": 50
  },
  "logs": [
    "string"
  ],
  "startup": {
    "runtime": "python",
    "runtimeVersion": "string",
    "entryFile": "string",
    "startCommand": "string"
  },
  "http": {
    "url": "https://.../files/download?path=%2Fmain.py",
    "status": 1,
    "ok": true,
    "contentType": "string",
    "title": "string",
    "bodyExcerpt": "string"
  },
  "hint": "string"
}
deployments_apply deployments:write

Make edited files live and confirm they work, in ONE call. Use this after writing files instead of deployments.power + deployments.logs + deployments.check. It restarts only when the runtime needs it (a static or PHP site serves straight off the volume, so the restart is skipped and `skippedRestart` says so), waits for the process to settle, then returns the same payload as deployments.diagnose. Pass restart: true to force one, restart: false to forbid it.

idrestart?waitSeconds?
Returns
{
  "state": "running",
  "installing": true,
  "resources": {
    "ramMB": 512,
    "ramLimitMB": 512,
    "cpuPercent": 50
  },
  "logs": [
    "string"
  ],
  "startup": {
    "runtime": "python",
    "runtimeVersion": "string",
    "entryFile": "string",
    "startCommand": "string"
  },
  "http": {
    "url": "https://.../files/download?path=%2Fmain.py",
    "status": 1,
    "ok": true,
    "contentType": "string",
    "title": "string",
    "bodyExcerpt": "string"
  },
  "hint": "string",
  "restarted": true,
  "skippedRestart": "string"
}
files_list files:read

List a directory inside the deployment volume.

idpath?
Returns
{
  "path": "/main.py",
  "entries": [
    {
      "name": "my-bot",
      "type": "file",
      "sizeBytes": 1048576,
      "modifiedAt": "2026-01-01T00:00:00.000Z",
      "mode": "0755"
    }
  ]
}
files_tree files:read

See the whole project layout in ONE call: every file and folder under a path, indented, with sizes. Use this FIRST when exploring, instead of walking directories with files.list. It returns structure only - read the files you want with files.readMany. Dependency folders (node_modules, vendor, .git, ...) are listed but not opened unless you pass includeVendor.

idpath?depth?includeVendor?maxEntries?
Returns
{
  "root": "/",
  "tree": "string",
  "files": 1,
  "directories": 1,
  "truncated": true,
  "note": "string"
}
files_readMany files:read

Read SEVERAL files in one call. Pair it with files.tree: look at the layout, then pull the handful of files you actually need in a single round instead of one call each. Binary files are reported, never returned as text.

idpathsmaxBytes?
Returns
{
  "files": [
    {
      "path": "/main.py",
      "content": "print(\"hello world\")",
      "bytes": 1048576,
      "skipped": "string"
    }
  ],
  "truncated": true,
  "note": "string"
}
files_search files:read

Search the text of the project for a string or regex, like grep. Returns matching lines with their file and line number. Use it to find where something is defined or used instead of reading files one by one.

idqueryregex?caseSensitive?path?glob?maxResults?
Returns
{
  "matches": [
    {
      "path": "/main.py",
      "line": 1,
      "text": "string"
    }
  ],
  "filesScanned": 1,
  "truncated": true,
  "note": "string"
}
files_edit files:write

Change PART of a file by replacing an exact snippet, leaving the rest untouched. Prefer this over files.write for any edit to an existing file: rewriting a whole file to change a few lines is slow, expensive and loses anything you did not repeat. The snippet must appear EXACTLY once unless you pass replaceAll.

idpatholdStringnewStringreplaceAll?
Returns
{
  "ok": true,
  "path": "/main.py",
  "replacements": 1,
  "bytes": 1048576,
  "lines": 1,
  "context": "string",
  "note": "string"
}
files_editMany files:write

Change SEVERAL places in ONE file in a single call. Use this instead of calling files.edit again and again on the same file: each files.edit is another round trip that re-sends the whole conversation. The edits apply in order, all of them or none: if one snippet is missing or ambiguous the file is left untouched and the error names the edit that failed, so you fix that one and send the list again.

idpathedits
Returns
{
  "ok": true,
  "path": "/main.py",
  "edits": 1,
  "replacements": 1,
  "bytes": 1048576,
  "lines": 1,
  "context": "string",
  "note": "string"
}
files_read files:read

Read a file, or a slice of it. Use offset/limit to page through a long file.

idpathoffset?limit?
Returns
{
  "path": "/main.py",
  "content": "print(\"hello world\")",
  "offset": 1,
  "lines": 1,
  "totalLines": 12,
  "hasMore": true,
  "note": "string"
}
files_write files:write

Create or overwrite a text file. A large file does NOT have to fit in one call: send the first part with mode 'overwrite', then the rest with mode 'append'.

idpathcontentmode?
Returns
{
  "ok": true,
  "path": "/main.py",
  "bytes": 1048576,
  "lines": 1,
  "note": "string",
  "hint": "string"
}
files_writeMany files:write

Create or overwrite SEVERAL files in ONE call. Use this when creating a project: index.html, style.css and package.json go in a single call instead of one files.write each, which costs a round trip per file. Files are written in the order given; if one fails the call stops and the reply says exactly which ones landed, so you never write the same file twice.

idfiles
Returns
{
  "ok": true,
  "written": [
    {
      "path": "/main.py",
      "bytes": 1048576,
      "lines": 1
    }
  ],
  "failed": [
    {
      "path": "/main.py",
      "error": "string"
    }
  ],
  "note": "string",
  "hint": "string"
}
files_rename files:write

Rename or MOVE a file or directory: from and to are paths relative to root, and may point into different directories.

idroot?fromto
Returns
{
  "ok": true
}
files_delete files:write destructive

Delete one or more files or folders. A copy of each deleted FILE is kept, so files.history / files.restore can bring it back; a deleted folder is gone for good.

idroot?files
Returns
{
  "ok": true,
  "deleted": 1
}
files_mkdir files:write

Create a folder.

idroot?name
Returns
{
  "ok": true
}
files_decompress files:write

Decompress an archive in place. With removeArchive the archive is deleted right after, in the same call, so an upload-and-unpack never leaves the archive behind.

idroot?fileremoveArchive?
Returns
{
  "ok": true,
  "archiveRemoved": true
}
files_compress files:write

Compress files into a new archive.

idroot?files
Returns
{
  "ok": true,
  "archive": "string"
}
files_copy files:write

Duplicate a file in place (the node appends a " copy" suffix). NOT a move: to move or rename, use files.rename.

idlocation
Returns
{
  "ok": true
}
files_chmod files:write

Change a file mode (e.g. "0755").

idroot?filemode
Returns
{
  "ok": true
}
files_downloadUrl files:read

Get a one-time signed URL to download a file.

idpath
Returns
{
  "url": "https://.../files/download?path=%2Fmain.py",
  "expiresAt": "2026-01-01T00:00:00.000Z"
}
files_index files:read

Flat index of every file under a path with size, mtime and an XXH64 content hash, in ONE call, for sync tools. Honours gitignore patterns. Send the `digest` of a listing you already hold: an unchanged tree answers `unchanged: true` with no entries.

idpath?ignore?hashMaxBytes?digest?
Returns
{
  "unchanged": true,
  "digest": "string",
  "truncated": true,
  "entries": [
    {
      "path": "/main.py",
      "type": "file",
      "sizeBytes": 1048576,
      "modifiedAt": "2026-01-01T00:00:00.000Z",
      "mode": "0755",
      "hash": "string"
    }
  ]
}
files_uploadUrl files:write

Get a one-time signed URL to upload files straight to the node, with no size cap from the panel. POST a multipart form with one or more `files` parts to it: each lands in `path` under its own file name. Valid 15 minutes, single use. Pair with files.decompress to unpack an archive.

idpath?
Returns
{
  "url": "https://.../files/download?path=%2Fmain.py",
  "field": "string",
  "expiresAt": "2026-01-01T00:00:00.000Z"
}
files_history files:read

List the saved previous versions of ONE file, newest first. Use it when a change broke something, when the user asks to undo or go back, or before overwriting a file you did not write yourself - then pass a versionId to files.restore. It only covers changes made through this API (write, edit, editMany, rename, delete): edits made over SFTP or by the running app itself are not tracked, and an empty list means there is nothing to undo.

idpath
Returns
{
  "path": "/main.py",
  "versions": [
    {
      "id": "dep_a1b2c3d4",
      "savedAt": "2026-01-01T00:00:00.000Z",
      "bytes": 1048576
    }
  ]
}
files_restore files:write destructive

Put a file back to one of its saved versions: this is the undo. Use it when an edit broke the app or the user asks to revert, and to bring back a file that was deleted. Call files.history first for the versionId. What it replaces is itself saved, so a restore can be undone in turn.

idpathversionId
Returns
{
  "ok": true,
  "path": "/main.py",
  "bytes": 1048576,
  "lines": 1,
  "context": "string"
}
env_list env:read

List environment variables. Secret values are masked.

id
Returns
{
  "variables": [
    {
      "key": "MY_VAR",
      "value": "some-value",
      "secret": true,
      "system": true
    }
  ]
}
env_set env:write

Create or update a user environment variable (applies on next restart).

idkeyvaluesecret?
Returns
{
  "ok": true,
  "key": "MY_VAR"
}
env_update env:write

Update an existing environment variable: rename it (newKey), change its value, or flip its secret flag. Applies on next restart.

idkeynewKey?value?secret?
Returns
{
  "ok": true,
  "key": "MY_VAR"
}
env_delete env:write destructive

Delete a user environment variable (applies on next restart).

idkey
Returns
{
  "ok": true
}
env_audit env:read

Compare the environment variables SET on the deployment with the ones the code actually reads. Use it right after writing code that needs configuration, and whenever an app crashes on boot for no visible reason: it names the variables the code reads but nothing sets (missing), the ones set but never read (unused), and the ones still holding an obvious placeholder. Returns keys only, never values.

id
Returns
{
  "used": [
    {
      "key": "MY_VAR",
      "files": [
        "string"
      ]
    }
  ],
  "missing": [
    "string"
  ],
  "unused": [
    "string"
  ],
  "placeholders": [
    "string"
  ],
  "note": "string"
}
backups_list backups:read

List the backups of a deployment.

id
Returns
{
  "backups": [
    {
      "id": "dep_a1b2c3d4",
      "deploymentId": "dep_a1b2c3d4",
      "label": "Manual",
      "sizeBytes": 1048576,
      "status": "active",
      "backupType": "manual",
      "fileCount": 2,
      "isOrphaned": true,
      "createdAt": "2026-01-01T00:00:00.000Z",
      "completedAt": "2026-01-01T00:00:00.000Z"
    }
  ]
}
backups_create backups:write

Start a manual backup of a deployment.

id
Returns
{
  "ok": true,
  "backupId": "bak_a1b2c3d4"
}
backups_get backups:read

Fetch a single backup by id.

backupId
Returns
{
  "id": "dep_a1b2c3d4",
  "deploymentId": "dep_a1b2c3d4",
  "label": "Manual",
  "sizeBytes": 1048576,
  "status": "active",
  "backupType": "manual",
  "fileCount": 2,
  "isOrphaned": true,
  "createdAt": "2026-01-01T00:00:00.000Z",
  "completedAt": "2026-01-01T00:00:00.000Z"
}
backups_delete backups:write destructive

Delete a backup.

backupId
Returns
{
  "ok": true
}
backups_restore backups:write destructive

Restore a backup onto a deployment (overwrites its files).

backupIddeploymentIdstartAfter?
Returns
{
  "ok": true,
  "warning": "string"
}
packages_list packages:read

List packages in the manifest (npm or pip).

idmanager
Returns
{
  "manager": "pip",
  "file": "archive.zip",
  "exists": true,
  "packages": [
    {
      "name": "my-bot",
      "spec": "==1.0.0",
      "dev": true
    }
  ]
}
packages_add packages:write

Add or update a package in the manifest.

idmanagernamespec?dev?
Returns
{
  "manager": "pip",
  "file": "archive.zip",
  "exists": true,
  "packages": [
    {
      "name": "my-bot",
      "spec": "==1.0.0",
      "dev": true
    }
  ]
}
packages_remove packages:write

Remove a package from the manifest.

idmanagername
Returns
{
  "manager": "pip",
  "file": "archive.zip",
  "exists": true,
  "packages": [
    {
      "name": "my-bot",
      "spec": "==1.0.0",
      "dev": true
    }
  ]
}
projects_list projects:read

List the projects you own or collaborate on.

Returns
{
  "projects": [
    {
      "id": "dep_a1b2c3d4",
      "name": "my-bot",
      "description": "A cool Discord bot",
      "isOwner": true,
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  ]
}
projects_create projects:write

Create a project.

namedescription?
Returns
{
  "id": "dep_a1b2c3d4",
  "name": "my-bot",
  "description": "A cool Discord bot",
  "isOwner": true,
  "createdAt": "2026-01-01T00:00:00.000Z"
}
projects_delete projects:write destructive

Delete a project and all its deployments (owner only).

id
Returns
{
  "ok": true
}
deployments_enableDomains deployments:write

Assign a subdomain (activate domains). Idempotent.

id
Returns
{
  "subdomain": "my-bot.apps",
  "url": "https://.../files/download?path=%2Fmain.py",
  "note": "string"
}
deployments_setSlug deployments:write

Set the subdomain alias (slug).

idslug
Returns
{
  "ok": true,
  "url": "https://.../files/download?path=%2Fmain.py"
}
deployments_removeSlug deployments:write

Remove the subdomain alias.

id
Returns
{
  "ok": true
}
deployments_setCustomDomain deployments:write

Attach a custom domain. Returns the DNS verification token.

iddomain
Returns
{
  "token": "string"
}
deployments_verifyCustomDomain deployments:write

Check the DNS for the attached custom domain.

id
Returns
{
  "verified": true,
  "reason": "string"
}
deployments_removeCustomDomain deployments:write

Detach the custom domain.

id
Returns
{
  "ok": true
}
deployments_check deployments:read

Request this deployment over its public URL and report what came back. Use it after deploying or fixing a site to confirm it really works - "the logs say it started" is not the same as "the page loads". Returns the HTTP status and the start of the body, so a 500 or a stack trace is visible. Server-rendered output only: no JavaScript runs. Any method: POST a JSON body to test an API route. On a 2xx HTML page it also requests the first same-origin links and assets it finds (`links`) and names the broken ones and duplicated nav entries in `note`. This is the ONLY way to send a request to the app; there is no shell and no curl. To ask what is wrong with a deployment rather than what one URL returns, call deployments.diagnose instead: it answers this plus the state, the logs and the startup config in one call.

idpath?method?body?headers?
Returns
{
  "url": "https://.../files/download?path=%2Fmain.py",
  "status": 1,
  "ok": true,
  "contentType": "string",
  "title": "string",
  "body": "string",
  "error": "string",
  "hint": "string",
  "links": [
    {
      "path": "/main.py",
      "status": 1
    }
  ],
  "note": "string"
}
docs_search docs:read

Search the Bot-Hosting documentation. Use this before answering any "how do I" question.

querylimit?
Returns
{
  "hits": [
    {
      "slug": "my-template",
      "title": "string",
      "section": "string",
      "snippet": "string"
    }
  ]
}
docs_get docs:read

Read one documentation page in full, by the slug returned from docs.search.

slug
Returns
{
  "slug": "my-template",
  "title": "string",
  "section": "string",
  "url": "https://.../files/download?path=%2Fmain.py",
  "content": "print(\"hello world\")"
}
files_outline files:read

Map the code WITHOUT reading it: every function, class, export and route with its line number, for one file or a whole folder. Call this BEFORE reading anything, to find where to look, then files.read only the part you need. It costs a fraction of reading the files and it returns no bodies at all. JavaScript, TypeScript, Python, PHP, Go and JSON.

idpath?
Returns
{
  "files": [
    {
      "path": "/main.py",
      "lines": 1,
      "symbols": [
        {
          "kind": "function",
          "name": "my-bot",
          "line": 1
        }
      ]
    }
  ],
  "truncated": true,
  "note": "string"
}

Account

account_get account:read

Your profile, credit balance, plan and quota (pool vs used).

Returns
{
  "id": "dep_a1b2c3d4",
  "username": "grality",
  "email": "[email protected]",
  "createdAt": "2026-01-01T00:00:00.000Z",
  "creditsCents": 5000,
  "plan": {
    "tier": "string",
    "name": "my-bot",
    "source": "catalog",
    "status": "active"
  },
  "quota": {
    "pool": {
      "ramMB": 512,
      "cpuPct": 50,
      "storageMB": 1024,
      "slots": 2
    },
    "used": {
      "ramMB": 512,
      "cpuPct": 50,
      "storageMB": 1024,
      "slots": 2
    }
  }
}

Templates

templates_list templates:read

Browse the public template catalogue (any author).

sort?category?q?page?perPage?
Returns
{
  "total": 12,
  "page": 2,
  "perPage": 2,
  "items": [
    {
      "id": "dep_a1b2c3d4",
      "slug": "my-template",
      "name": "my-bot",
      "tagline": "A cool Discord bot",
      "category": "utility",
      "runtime": "python",
      "githubRepo": "string",
      "githubStars": 3,
      "deployCount": 3,
      "imageId": "dep_a1b2c3d4",
      "owner": {
        "username": "grality",
        "avatar": "string"
      },
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  ]
}
templates_get templates:read

Full details + public stats of a single template by slug.

slug
Returns
{
  "id": "dep_a1b2c3d4",
  "slug": "my-template",
  "name": "my-bot",
  "tagline": "A cool Discord bot",
  "category": "utility",
  "runtime": "python",
  "githubRepo": "string",
  "githubStars": 3,
  "deployCount": 3,
  "imageId": "dep_a1b2c3d4",
  "owner": {
    "username": "grality",
    "avatar": "string"
  },
  "createdAt": "2026-01-01T00:00:00.000Z",
  "branch": "string",
  "runtimeVersion": "string",
  "readme": "# My template",
  "stats": {
    "views": 3,
    "deploys": 3,
    "likes": 3,
    "dislikes": 3,
    "githubStars": 3,
    "trendingScore": 1.5
  },
  "envSchema": [
    {
      "key": "MY_VAR",
      "label": "Manual",
      "description": "A cool Discord bot",
      "required": true,
      "secret": true,
      "defaultValue": "string"
    }
  ],
  "updatedAt": "2026-01-01T00:00:00.000Z"
}

Prefer raw HTTP?

The same operations are a plain REST API with curl, Python and Node examples.

REST API docs