API Documentation
A REST API to drive your account. 71 endpoints across 3 categories.
Public beta
The API is open in beta. Build, experiment and have fun with it, but use it with care: endpoints and responses may still change, and every call acts on your real deployments.
Authentication
Every request needs an API key. Pass it as a Bearer token. Create and manage keys from your API settings.
curl -H "Authorization: Bearer bhk_your_key" \
https://bot-hosting.net/api/v1/deploymentsScopes
A key is limited to the scopes you grant it. A request is allowed only if the key holds the endpoint's scope and you have permission on the target resource.
Deployments
Account
Templates
Documentation
Rate limits
Requests are capped at 120 per minute per key (a sliding 60s window). Every response carries the remaining budget; going over returns 429 with a Retry-After header.
HTTP/1.1 200 OK X-RateLimit-Remaining: 118 # once exceeded: HTTP/1.1 429 Too Many Requests Retry-After: 42
AI / MCP
The same operations are exposed as an MCP server, so an AI assistant (Claude Desktop, Cursor, an in-app agent) can drive your account with natural language. Setup, the full tool catalog and examples live on their own page:
MCP documentationDeployment state
The state field is read live from the node on every request, so it reflects what your bot is doing right now, including a crash or a stop you did not trigger. Do not confuse it with status, which is the billing lifecycle (active / suspended).
Networking
Every deployment carries the node it runs on and the ports it publishes. To reach a service from outside, connect to node.fqdn:port.
Access & sharing
list and get return both the deployments you own and those shared with you. Three fields tell them apart.
Deployments
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).
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/sftp" \ -H "Authorization: Bearer bhk_your_key"
{
"host": "string",
"port": 25565,
"username": "grality",
"password": "string"
}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
- Keep only deployments whose name contains this, case-insensitive.
- brief
- Return the short row instead of the full record.
curl "https://bot-hosting.net/api/v1/deployments?name=my-bot&brief=true" \ -H "Authorization: Bearer bhk_your_key"
{
"deployments": [
"string"
]
}Fetch a single deployment.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4" \ -H "Authorization: Bearer bhk_your_key"
{
"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"
}
}Rename a deployment or change its description.
curl -X PATCH "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"my-bot","description":"A cool Discord bot"}'{
"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
]
}Move a deployment to another of your projects.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/move" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"toProjectId":"dep_a1b2c3d4"}'{
"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
]
}Delete a deployment (instant; teardown runs in the background).
curl -X DELETE "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4" \ -H "Authorization: Bearer bhk_your_key"
{
"ok": true
}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.
- ramMB
- Omit for the default share of the free pool.
- cpuPct
- Percent of one core. 15 makes an npm install take minutes.
- projectId
- Existing project id. OMIT THIS to use your default project - do not invent one.
- source
- One of: blank, github, template.
- githubRepo
- "owner/repo" or the GitHub URL of a public repository.
- branch
- Omit to use the repository's default branch (main, master, or whatever it is).
- env
- Environment variables to set on the new deployment. For a template, these fill its declared variables.
- runtime
- One of: nodejs, python, java, php, go, rust, cpp, lua, nodemon, bun, redis, postgresql, mongodb, mysql, static, phpweb, lavalink, uptimekuma.
- runtimeVersion
- Runtime version as a string, e.g. "24"; a bare number is accepted too.
curl -X POST "https://bot-hosting.net/api/v1/deployments" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"my-bot","ramMB":512,"cpuPct":50,"storageMB":1024,"description":"A cool Discord bot","projectId":"dep_a1b2c3d4","source":"blank","githubRepo":"string","branch":"string","templateId":"dep_a1b2c3d4","env":"string","runtime":"nodejs","runtimeVersion":"string"}'{
"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"
}Get the startup config: runtime, version, entry file, start command.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/startup" \ -H "Authorization: Bearer bhk_your_key"
{
"kind": "string",
"runtime": "python",
"runtimeVersion": "string",
"entryFile": "string",
"startCommand": "string",
"engine": "string"
}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.
- runtime
- Omit to keep the current runtime. One of: nodejs, python, java, php, go, rust, cpp, lua, nodemon, bun, redis, postgresql, mongodb, mysql, static, phpweb, lavalink, uptimekuma.
- runtimeVersion
- Runtime version as a string, e.g. "24"; a bare number is accepted too. Omit to keep the current one.
- entryFile
- Omit to keep the current entry file.
- startCommand
- Full shell line, run in the container at every boot, from the volume root (/home/container, where files.tree starts): never cd to /app or any other path. Chain install/build/start with &&. A server must bind 0.0.0.0 and the SERVER_PORT env (or pass the port explicitly). The last step is exec'd so that Stop works, and exec is added for you if you leave it out. null = the runtime default, which already installs from package.json / requirements.txt. Omit to keep the current command.
- kind
- One of: runtime, database, service.
curl -X PATCH "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/startup" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"runtime":"nodejs","runtimeVersion":"string","entryFile":"string","startCommand":"string","kind":"runtime","engine":"string"}'{
"kind": "string",
"runtime": "python",
"runtimeVersion": "string",
"entryFile": "string",
"startCommand": "string",
"engine": "string",
"warning": "string"
}List available runtimes, services and database engines with their versions.
curl "https://bot-hosting.net/api/v1/runtimes" \ -H "Authorization: Bearer bhk_your_key"
{
"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"
}
]
}Get the linked GitHub source: repo, branch, auto-pull.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/git" \ -H "Authorization: Bearer bhk_your_key"
{
"linked": true,
"repo": "string",
"branch": "string",
"autoPull": true
}Toggle auto-pull: re-pull the linked repo on every restart.
curl -X PATCH "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/git" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"autoPull":true}'{
"linked": true,
"repo": "string",
"branch": "string",
"autoPull": true
}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.
- action
- One of: start, stop, restart, kill.
- waitSeconds
- How long to wait for the process to settle before reading back. Default 20 on start and restart, 0 on stop and kill.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/power" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"action":"start","waitSeconds":1}'{
"ok": true,
"action": "restart",
"reason": "string",
"state": "running",
"logs": [
"string"
],
"hint": "string"
}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.
- size
- Default 100.
- waitSeconds
- Wait up to this long BEFORE reading, returning early once the process has died or gone idle (no new output, CPU quiet). The wait is server-side and costs nothing. settled:false at the end means it is still working (installing): wait again.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/logs?size=1048576&waitSeconds=1" \ -H "Authorization: Bearer bhk_your_key"
{
"lines": [
"Bot is online!"
],
"state": "running",
"settled": true,
"hint": "string"
}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.
- pattern
- A word, or a regular expression. Case-insensitive.
- lines
- How far back to look, in lines. Default 2000.
- context
- Lines to return around each hit. Default 2.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/logs/search?pattern=string&lines=1&context=1" \ -H "Authorization: Bearer bhk_your_key"
{
"matches": [
{
"line": 1,
"text": "string",
"around": [
"string"
]
}
],
"scanned": 1,
"truncated": true
}Live resource usage: CPU, memory, disk, network and uptime.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/resources" \ -H "Authorization: Bearer bhk_your_key"
{
"state": "running",
"cpu": {
"usedPercent": 50,
"limitPercent": 50
},
"memory": {
"usedBytes": 1048576,
"limitBytes": 1048576
},
"disk": {
"usedBytes": 1048576,
"limitBytes": 1048576
},
"network": {
"rxBytes": 1048576,
"txBytes": 1048576
},
"uptimeMs": 3600000
}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.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/command" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"command":"say hello"}'{
"ok": true,
"note": "string"
}Change RAM / CPU / storage allocation (drawn from your plan pool).
- ramMB
- Megabytes. npm install of a modern stack needs 512+; a Svelte/Next build 1024+. At the 256 default, install is SIGKILLed: exit 137, "Out of memory: true".
- storageMB
- Megabytes. node_modules of a full-stack app is often 500-1500 MB on its own.
curl -X PATCH "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/resize" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"ramMB":512,"cpuPct":50,"storageMB":1024}'{
"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
]
}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.
- repo
- Repoint at another repository, "owner/repo". Omit to keep the current one.
- branch
- Switch to this branch before pulling. Omit to keep the current one.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/sync" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"repo":"string","branch":"string"}'{
"ok": true,
"commit": "string",
"repo": "string",
"branch": "string"
}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.
- waitSeconds
- Wait up to this long for the process to settle BEFORE reading, returning early once it has died or gone idle. Pass 45 when something is still installing or starting; the wait is server-side and costs nothing.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/diagnose?waitSeconds=1" \ -H "Authorization: Bearer bhk_your_key"
{
"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"
}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.
- restart
- Force a restart (true) or forbid one (false). Omit to let the runtime decide.
- waitSeconds
- How long to wait for the process to settle. Default 20 after a restart, 0 without one.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/apply" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"restart":true,"waitSeconds":1}'{
"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"
}List a directory inside the deployment volume.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files?path=%2Fmain.py" \ -H "Authorization: Bearer bhk_your_key"
{
"path": "/main.py",
"entries": [
{
"name": "my-bot",
"type": "file",
"sizeBytes": 1048576,
"modifiedAt": "2026-01-01T00:00:00.000Z",
"mode": "0755"
}
]
}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.
- path
- Directory to start from. Defaults to the volume root.
- depth
- How many directory levels to descend. Default 6.
- includeVendor
- Also walk INTO node_modules, vendor, .git and friends. Default false - they hold thousands of files. Point `path` at one instead to look inside just that one.
- maxEntries
- Default 400.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/tree?path=%2Fmain.py&depth=1&includeVendor=true&maxEntries=1" \ -H "Authorization: Bearer bhk_your_key"
{
"root": "/",
"tree": "string",
"files": 1,
"directories": 1,
"truncated": true,
"note": "string"
}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.
- paths
- Absolute paths inside the volume, as shown by files.tree.
- maxBytes
- Total budget across all files. Default 120000. Files past it come back unread.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/read-many" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"paths":["string"],"maxBytes":1048576}'{
"files": [
{
"path": "/main.py",
"content": "print(\"hello world\")",
"bytes": 1048576,
"skipped": "string"
}
],
"truncated": true,
"note": "string"
}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.
- query
- Text to find, or a regular expression when regex is true.
- regex
- Treat query as a JavaScript regular expression. Default false.
- caseSensitive
- Default false.
- path
- Limit the search to this directory, or to a single file. Defaults to the whole volume.
- glob
- Only files whose name matches, e.g. '*.php' or '*.{ts,js}'. Omit to search every text file.
- maxResults
- Default 100.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/search?query=string®ex=true&caseSensitive=true&path=%2Fmain.py&glob=string&maxResults=1" \ -H "Authorization: Bearer bhk_your_key"
{
"matches": [
{
"path": "/main.py",
"line": 1,
"text": "string"
}
],
"filesScanned": 1,
"truncated": true,
"note": "string"
}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.
- path
- REQUIRED. The file to edit, volume-root relative, e.g. '/index.js'.
- oldString
- The exact text to replace, copied from the file including its indentation.
- newString
- What to put in its place. An empty string deletes the snippet.
- replaceAll
- Replace every occurrence instead of requiring exactly one.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/edit" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"/main.py","oldString":"string","newString":"string","replaceAll":true}'{
"ok": true,
"path": "/main.py",
"replacements": 1,
"bytes": 1048576,
"lines": 1,
"context": "string",
"note": "string"
}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.
- path
- REQUIRED. The one file every edit applies to, volume-root relative, e.g. '/index.js'.
- edits
- Applied in order to the same file. Two edits must not touch the same text: once an edit has replaced a snippet, a later edit can no longer match it.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/editMany" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"/main.py","edits":[{"oldString":"string","newString":"string","replaceAll":true}]}'{
"ok": true,
"path": "/main.py",
"edits": 1,
"replacements": 1,
"bytes": 1048576,
"lines": 1,
"context": "string",
"note": "string"
}Read a file, or a slice of it. Use offset/limit to page through a long file.
- offset
- First line to return, 0-based.
- limit
- How many lines. Default 400.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/content?path=%2Fmain.py&offset=1&limit=1" \ -H "Authorization: Bearer bhk_your_key"
{
"path": "/main.py",
"content": "print(\"hello world\")",
"offset": 1,
"lines": 1,
"totalLines": 12,
"hasMore": true,
"note": "string"
}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'.
- path
- Volume-root relative, e.g. '/src/index.js' (the volume root is /home/container inside the container).
- content
- The text to write, as a string. A JSON object is accepted and stored stringified. Required - a call with no content writes nothing.
- mode
- 'overwrite' (default) replaces the file. 'append' adds to the end, so a long file can be written across several calls instead of one huge one. One of: overwrite, append.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/content" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"/main.py","content":"print(\"hello world\")","mode":"overwrite"}'{
"ok": true,
"path": "/main.py",
"bytes": 1048576,
"lines": 1,
"note": "string",
"hint": "string"
}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.
- files
- Written in order. All the content together is capped at 5 MB: split a bigger batch across calls.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/writeMany" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"files":[{"path":"/main.py","content":"print(\"hello world\")","mode":"overwrite"}]}'{
"ok": true,
"written": [
{
"path": "/main.py",
"bytes": 1048576,
"lines": 1
}
],
"failed": [
{
"path": "/main.py",
"error": "string"
}
],
"note": "string",
"hint": "string"
}Rename or MOVE a file or directory: from and to are paths relative to root, and may point into different directories.
- root
- Directory both paths are relative to. Default '/'.
- from
- Current path, e.g. 'app/main.py'.
- to
- New path, e.g. 'main.py' to move it to the root.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/rename" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"root":"/","from":"old.py","to":"new.py"}'{
"ok": true
}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.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/delete" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"root":"/","files":["string"]}'{
"ok": true,
"deleted": 1
}Create a folder.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/folder" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"root":"/","name":"my-bot"}'{
"ok": true
}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.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/decompress" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"root":"/","file":"archive.zip","removeArchive":true}'{
"ok": true,
"archiveRemoved": true
}Compress files into a new archive.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/compress" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"root":"/","files":["string"]}'{
"ok": true,
"archive": "string"
}Duplicate a file in place (the node appends a " copy" suffix). NOT a move: to move or rename, use files.rename.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/copy" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"location":"string"}'{
"ok": true
}Change a file mode (e.g. "0755").
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/chmod" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"root":"/","file":"archive.zip","mode":"0755"}'{
"ok": true
}Get a one-time signed URL to download a file.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/download-url?path=%2Fmain.py" \ -H "Authorization: Bearer bhk_your_key"
{
"url": "https://.../files/download?path=%2Fmain.py",
"expiresAt": "2026-01-01T00:00:00.000Z"
}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.
- path
- Directory to index. Defaults to the volume root.
- ignore
- gitignore-style patterns. A matching directory is not entered.
- hashMaxBytes
- Files above this size are listed without a hash. Default 4 MiB, 0 disables hashing.
- digest
- Digest of a previous index of the same path.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/index" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"/main.py","ignore":["string"],"hashMaxBytes":1048576,"digest":"string"}'{
"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"
}
]
}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.
- path
- Directory the uploaded files land in. Defaults to the volume root.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/upload-url" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"/main.py"}'{
"url": "https://.../files/download?path=%2Fmain.py",
"field": "string",
"expiresAt": "2026-01-01T00:00:00.000Z"
}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.
- path
- The file to look up, volume-root relative, e.g. '/index.js'.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/history?path=%2Fmain.py" \ -H "Authorization: Bearer bhk_your_key"
{
"path": "/main.py",
"versions": [
{
"id": "dep_a1b2c3d4",
"savedAt": "2026-01-01T00:00:00.000Z",
"bytes": 1048576
}
]
}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.
- path
- The file to put back, volume-root relative, e.g. '/index.js'.
- versionId
- The id of the version to restore, exactly as files.history returned it.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/restore" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"path":"/main.py","versionId":"dep_a1b2c3d4"}'{
"ok": true,
"path": "/main.py",
"bytes": 1048576,
"lines": 1,
"context": "string"
}List environment variables. Secret values are masked.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/env" \ -H "Authorization: Bearer bhk_your_key"
{
"variables": [
{
"key": "MY_VAR",
"value": "some-value",
"secret": true,
"system": true
}
]
}Create or update a user environment variable (applies on next restart).
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/env" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"key":"MY_VAR","value":"some-value","secret":true}'{
"ok": true,
"key": "MY_VAR"
}Update an existing environment variable: rename it (newKey), change its value, or flip its secret flag. Applies on next restart.
curl -X PATCH "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/env/MY_VAR" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"newKey":"MY_RENAMED_VAR","value":"some-value","secret":true}'{
"ok": true,
"key": "MY_VAR"
}Delete a user environment variable (applies on next restart).
curl -X DELETE "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/env/MY_VAR" \ -H "Authorization: Bearer bhk_your_key"
{
"ok": true
}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.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/env/audit" \ -H "Authorization: Bearer bhk_your_key"
{
"used": [
{
"key": "MY_VAR",
"files": [
"string"
]
}
],
"missing": [
"string"
],
"unused": [
"string"
],
"placeholders": [
"string"
],
"note": "string"
}List the backups of a deployment.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/backups" \ -H "Authorization: Bearer bhk_your_key"
{
"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"
}
]
}Start a manual backup of a deployment.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/backups" \ -H "Authorization: Bearer bhk_your_key"
{
"ok": true,
"backupId": "bak_a1b2c3d4"
}Fetch a single backup by id.
curl "https://bot-hosting.net/api/v1/backups/bak_a1b2c3d4" \ -H "Authorization: Bearer bhk_your_key"
{
"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"
}Delete a backup.
curl -X DELETE "https://bot-hosting.net/api/v1/backups/bak_a1b2c3d4" \ -H "Authorization: Bearer bhk_your_key"
{
"ok": true
}Restore a backup onto a deployment (overwrites its files).
curl -X POST "https://bot-hosting.net/api/v1/backups/bak_a1b2c3d4/restore" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"deploymentId":"dep_a1b2c3d4","startAfter":true}'{
"ok": true,
"warning": "string"
}List packages in the manifest (npm or pip).
- manager
- One of: npm, pip.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/packages?manager=npm" \ -H "Authorization: Bearer bhk_your_key"
{
"manager": "pip",
"file": "archive.zip",
"exists": true,
"packages": [
{
"name": "my-bot",
"spec": "==1.0.0",
"dev": true
}
]
}Add or update a package in the manifest.
- manager
- One of: npm, pip.
- spec
- Version range, e.g. "^14.16.3". Omit to pin the registry's current version.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/packages" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"manager":"npm","name":"my-bot","spec":"==1.0.0","dev":true}'{
"manager": "pip",
"file": "archive.zip",
"exists": true,
"packages": [
{
"name": "my-bot",
"spec": "==1.0.0",
"dev": true
}
]
}Remove a package from the manifest.
- manager
- One of: npm, pip.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/packages/remove" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"manager":"npm","name":"my-bot"}'{
"manager": "pip",
"file": "archive.zip",
"exists": true,
"packages": [
{
"name": "my-bot",
"spec": "==1.0.0",
"dev": true
}
]
}List the projects you own or collaborate on.
curl "https://bot-hosting.net/api/v1/projects" \ -H "Authorization: Bearer bhk_your_key"
{
"projects": [
{
"id": "dep_a1b2c3d4",
"name": "my-bot",
"description": "A cool Discord bot",
"isOwner": true,
"createdAt": "2026-01-01T00:00:00.000Z"
}
]
}Create a project.
curl -X POST "https://bot-hosting.net/api/v1/projects" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"name":"my-bot","description":"A cool Discord bot"}'{
"id": "dep_a1b2c3d4",
"name": "my-bot",
"description": "A cool Discord bot",
"isOwner": true,
"createdAt": "2026-01-01T00:00:00.000Z"
}Delete a project and all its deployments (owner only).
curl -X DELETE "https://bot-hosting.net/api/v1/projects/dep_a1b2c3d4" \ -H "Authorization: Bearer bhk_your_key"
{
"ok": true
}Assign a subdomain (activate domains). Idempotent.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains" \ -H "Authorization: Bearer bhk_your_key"
{
"subdomain": "my-bot.apps",
"url": "https://.../files/download?path=%2Fmain.py",
"note": "string"
}Set the subdomain alias (slug).
curl -X PATCH "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains/slug" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"slug":"my-template"}'{
"ok": true,
"url": "https://.../files/download?path=%2Fmain.py"
}Remove the subdomain alias.
curl -X DELETE "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains/slug" \ -H "Authorization: Bearer bhk_your_key"
{
"ok": true
}Attach a custom domain. Returns the DNS verification token.
curl -X PATCH "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains/custom" \
-H "Authorization: Bearer bhk_your_key" \
-H "Content-Type: application/json" \
-d '{"domain":"string"}'{
"token": "string"
}Check the DNS for the attached custom domain.
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains/custom/verify" \ -H "Authorization: Bearer bhk_your_key"
{
"verified": true,
"reason": "string"
}Detach the custom domain.
curl -X DELETE "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains/custom" \ -H "Authorization: Bearer bhk_your_key"
{
"ok": true
}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.
- path
- Path to request, e.g. '/' or '/api/scores'. Defaults to '/'.
- method
- Default GET. One of: GET, POST, PUT, PATCH, DELETE.
- body
- Request body. An object is sent as JSON (Content-Type set for you); a string is sent as is.
- headers
- Extra request headers, e.g. Authorization.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/check?path=%2Fmain.py&method=GET&body=string&headers=string" \ -H "Authorization: Bearer bhk_your_key"
{
"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"
}Search the Bot-Hosting documentation. Use this before answering any "how do I" question.
- query
- The question, in natural language. Semantic search: ask it plainly.
curl "https://bot-hosting.net/api/v1/docs/search?query=string&limit=1" \ -H "Authorization: Bearer bhk_your_key"
{
"hits": [
{
"slug": "my-template",
"title": "string",
"section": "string",
"snippet": "string"
}
]
}Read one documentation page in full, by the slug returned from docs.search.
curl "https://bot-hosting.net/api/v1/docs/my-template" \ -H "Authorization: Bearer bhk_your_key"
{
"slug": "my-template",
"title": "string",
"section": "string",
"url": "https://.../files/download?path=%2Fmain.py",
"content": "print(\"hello world\")"
}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.
- path
- A file, or a directory to outline every source file under. Defaults to the project root.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/outline?path=%2Fmain.py" \ -H "Authorization: Bearer bhk_your_key"
{
"files": [
{
"path": "/main.py",
"lines": 1,
"symbols": [
{
"kind": "function",
"name": "my-bot",
"line": 1
}
]
}
],
"truncated": true,
"note": "string"
}Account
Your profile, credit balance, plan and quota (pool vs used).
curl "https://bot-hosting.net/api/v1/account" \ -H "Authorization: Bearer bhk_your_key"
{
"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
Browse the public template catalogue (any author).
- sort
- One of: trending, deployed, rated, newest.
- category
- Filter to one category. Omit to browse everything. One of: moderation, music, economy, leveling, tickets, ai, games, utility, security, welcome, logging, social, dashboards, apis, automation, notifications, finance, roleplay, giveaways, other.
curl "https://bot-hosting.net/api/v1/templates?sort=trending&category=moderation&q=search&page=2&perPage=2" \ -H "Authorization: Bearer bhk_your_key"
{
"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"
}
]
}Full details + public stats of a single template by slug.
curl "https://bot-hosting.net/api/v1/templates/my-template" \ -H "Authorization: Bearer bhk_your_key"
{
"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"
}