Skip to content

API Documentation

A REST API to drive your account. 71 endpoints across 3 categories.

Manage API keys

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/deployments

Scopes

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

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

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 documentation

Deployment 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).

running Up and serving right now.
starting Boot requested, container not up yet.
stopping Shutdown in progress.
offline Installed but not running (stopped, crashed, or out of resources).
installing Created and being provisioned; it has never run yet.
unknown Its node could not be reached, so we will not guess. Retry shortly.

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.

node The node hosting it (name, fqdn to the host to connect to to and region). null until it is placed on a node.
port The primary published port. null if none is published yet.
ports Every published port, primary first.

Access & sharing

list and get return both the deployments you own and those shared with you. Three fields tell them apart.

owned true if you own it, false if it was shared with you.
owner Who owns it (id + username). Always present.
shared Collaborators, each with their granular permissions (OVERVIEW, CONSOLE, FILES, ADMINISTRATOR…). Empty if not shared.

Deployments

GET /deployments/:id/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:path
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/sftp" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "host": "string",
  "port": 25565,
  "username": "grality",
  "password": "string"
}
GET /deployments 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:query?brief:query?
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"
Response 200
{
  "deployments": [
    "string"
  ]
}
GET /deployments/:id deployments:read

Fetch a single deployment.

id:path
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "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"
  }
}
PATCH /deployments/:id deployments:write

Rename a deployment or change its description.

id:pathname:body?description:body?
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"}'
Response 200
{
  "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
  ]
}
POST /deployments/:id/move deployments:write

Move a deployment to another of your projects.

id:pathtoProjectId:body
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"}'
Response 200
{
  "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 /deployments/:id deployments:write destructive

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

id:path
curl -X DELETE "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "ok": true
}
POST /deployments 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.

name:bodyramMB:body?cpuPct:body?storageMB:body?description:body?projectId:body?source:bodygithubRepo:body?branch:body?templateId:body?env:body?runtime:body?runtimeVersion:body?
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"}'
Response 200
{
  "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 /deployments/:id/startup deployments:read

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

id:path
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/startup" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "kind": "string",
  "runtime": "python",
  "runtimeVersion": "string",
  "entryFile": "string",
  "startCommand": "string",
  "engine": "string"
}
PATCH /deployments/:id/startup 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.

id:pathruntime:body?runtimeVersion:body?entryFile:body?startCommand:body?kind:body?engine:body?
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"}'
Response 200
{
  "kind": "string",
  "runtime": "python",
  "runtimeVersion": "string",
  "entryFile": "string",
  "startCommand": "string",
  "engine": "string",
  "warning": "string"
}
GET /runtimes deployments:read

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

curl "https://bot-hosting.net/api/v1/runtimes" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "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 /deployments/:id/git deployments:read

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

id:path
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/git" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "linked": true,
  "repo": "string",
  "branch": "string",
  "autoPull": true
}
PATCH /deployments/:id/git deployments:write

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

id:pathautoPull:body
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}'
Response 200
{
  "linked": true,
  "repo": "string",
  "branch": "string",
  "autoPull": true
}
POST /deployments/:id/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.

id:pathaction:bodywaitSeconds:body?
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}'
Response 200
{
  "ok": true,
  "action": "restart",
  "reason": "string",
  "state": "running",
  "logs": [
    "string"
  ],
  "hint": "string"
}
GET /deployments/:id/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.

id:pathsize:query?waitSeconds:query?
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"
Response 200
{
  "lines": [
    "Bot is online!"
  ],
  "state": "running",
  "settled": true,
  "hint": "string"
}
GET /deployments/:id/logs/search 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.

id:pathpattern:querylines:query?context:query?
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"
Response 200
{
  "matches": [
    {
      "line": 1,
      "text": "string",
      "around": [
        "string"
      ]
    }
  ],
  "scanned": 1,
  "truncated": true
}
GET /deployments/:id/resources deployments:read

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

id:path
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/resources" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "state": "running",
  "cpu": {
    "usedPercent": 50,
    "limitPercent": 50
  },
  "memory": {
    "usedBytes": 1048576,
    "limitBytes": 1048576
  },
  "disk": {
    "usedBytes": 1048576,
    "limitBytes": 1048576
  },
  "network": {
    "rxBytes": 1048576,
    "txBytes": 1048576
  },
  "uptimeMs": 3600000
}
POST /deployments/:id/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.

id:pathcommand:body
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"}'
Response 200
{
  "ok": true,
  "note": "string"
}
PATCH /deployments/:id/resize deployments:write

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

id:pathramMB:bodycpuPct:bodystorageMB:body
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}'
Response 200
{
  "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
  ]
}
POST /deployments/:id/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.

id:pathrepo:body?branch:body?
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"}'
Response 200
{
  "ok": true,
  "commit": "string",
  "repo": "string",
  "branch": "string"
}
GET /deployments/:id/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.

id:pathwaitSeconds:query?
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"
Response 200
{
  "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"
}
POST /deployments/:id/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.

id:pathrestart:body?waitSeconds:body?
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}'
Response 200
{
  "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"
}
GET /deployments/:id/files files:read

List a directory inside the deployment volume.

id:pathpath:query?
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files?path=%2Fmain.py" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "path": "/main.py",
  "entries": [
    {
      "name": "my-bot",
      "type": "file",
      "sizeBytes": 1048576,
      "modifiedAt": "2026-01-01T00:00:00.000Z",
      "mode": "0755"
    }
  ]
}
GET /deployments/:id/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.

id:pathpath:query?depth:query?includeVendor:query?maxEntries:query?
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"
Response 200
{
  "root": "/",
  "tree": "string",
  "files": 1,
  "directories": 1,
  "truncated": true,
  "note": "string"
}
POST /deployments/:id/files/read-many 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.

id:pathpaths:bodymaxBytes:body?
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}'
Response 200
{
  "files": [
    {
      "path": "/main.py",
      "content": "print(\"hello world\")",
      "bytes": 1048576,
      "skipped": "string"
    }
  ],
  "truncated": true,
  "note": "string"
}
POST /deployments/:id/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.

id:pathpath:bodyoldString:bodynewString:bodyreplaceAll:body?
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}'
Response 200
{
  "ok": true,
  "path": "/main.py",
  "replacements": 1,
  "bytes": 1048576,
  "lines": 1,
  "context": "string",
  "note": "string"
}
POST /deployments/:id/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.

id:pathpath:bodyedits:body
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}]}'
Response 200
{
  "ok": true,
  "path": "/main.py",
  "edits": 1,
  "replacements": 1,
  "bytes": 1048576,
  "lines": 1,
  "context": "string",
  "note": "string"
}
GET /deployments/:id/files/content files:read

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

id:pathpath:queryoffset:query?limit:query?
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"
Response 200
{
  "path": "/main.py",
  "content": "print(\"hello world\")",
  "offset": 1,
  "lines": 1,
  "totalLines": 12,
  "hasMore": true,
  "note": "string"
}
POST /deployments/:id/files/content 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'.

id:pathpath:bodycontent:bodymode:body?
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"}'
Response 200
{
  "ok": true,
  "path": "/main.py",
  "bytes": 1048576,
  "lines": 1,
  "note": "string",
  "hint": "string"
}
POST /deployments/:id/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.

id:pathfiles:body
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"}]}'
Response 200
{
  "ok": true,
  "written": [
    {
      "path": "/main.py",
      "bytes": 1048576,
      "lines": 1
    }
  ],
  "failed": [
    {
      "path": "/main.py",
      "error": "string"
    }
  ],
  "note": "string",
  "hint": "string"
}
POST /deployments/:id/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.

id:pathroot:body?from:bodyto:body
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"}'
Response 200
{
  "ok": true
}
POST /deployments/:id/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.

id:pathroot:body?files:body
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"]}'
Response 200
{
  "ok": true,
  "deleted": 1
}
POST /deployments/:id/files/folder files:write

Create a folder.

id:pathroot:body?name:body
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"}'
Response 200
{
  "ok": true
}
POST /deployments/:id/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.

id:pathroot:body?file:bodyremoveArchive:body?
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}'
Response 200
{
  "ok": true,
  "archiveRemoved": true
}
POST /deployments/:id/files/compress files:write

Compress files into a new archive.

id:pathroot:body?files:body
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"]}'
Response 200
{
  "ok": true,
  "archive": "string"
}
POST /deployments/:id/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.

id:pathlocation:body
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"}'
Response 200
{
  "ok": true
}
POST /deployments/:id/files/chmod files:write

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

id:pathroot:body?file:bodymode:body
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"}'
Response 200
{
  "ok": true
}
GET /deployments/:id/files/download-url files:read

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

id:pathpath:query
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/files/download-url?path=%2Fmain.py" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "url": "https://.../files/download?path=%2Fmain.py",
  "expiresAt": "2026-01-01T00:00:00.000Z"
}
POST /deployments/:id/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.

id:pathpath:body?ignore:body?hashMaxBytes:body?digest:body?
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"}'
Response 200
{
  "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"
    }
  ]
}
POST /deployments/:id/files/upload-url 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.

id:pathpath:body?
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"}'
Response 200
{
  "url": "https://.../files/download?path=%2Fmain.py",
  "field": "string",
  "expiresAt": "2026-01-01T00:00:00.000Z"
}
GET /deployments/:id/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.

id:pathpath:query
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"
Response 200
{
  "path": "/main.py",
  "versions": [
    {
      "id": "dep_a1b2c3d4",
      "savedAt": "2026-01-01T00:00:00.000Z",
      "bytes": 1048576
    }
  ]
}
POST /deployments/:id/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.

id:pathpath:bodyversionId:body
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"}'
Response 200
{
  "ok": true,
  "path": "/main.py",
  "bytes": 1048576,
  "lines": 1,
  "context": "string"
}
GET /deployments/:id/env env:read

List environment variables. Secret values are masked.

id:path
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/env" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "variables": [
    {
      "key": "MY_VAR",
      "value": "some-value",
      "secret": true,
      "system": true
    }
  ]
}
POST /deployments/:id/env env:write

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

id:pathkey:bodyvalue:bodysecret:body?
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}'
Response 200
{
  "ok": true,
  "key": "MY_VAR"
}
PATCH /deployments/:id/env/:key env:write

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

id:pathkey:pathnewKey:body?value:body?secret:body?
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}'
Response 200
{
  "ok": true,
  "key": "MY_VAR"
}
DELETE /deployments/:id/env/:key env:write destructive

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

id:pathkey:path
curl -X DELETE "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/env/MY_VAR" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "ok": true
}
GET /deployments/:id/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:path
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/env/audit" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "used": [
    {
      "key": "MY_VAR",
      "files": [
        "string"
      ]
    }
  ],
  "missing": [
    "string"
  ],
  "unused": [
    "string"
  ],
  "placeholders": [
    "string"
  ],
  "note": "string"
}
GET /deployments/:id/backups backups:read

List the backups of a deployment.

id:path
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/backups" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "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"
    }
  ]
}
POST /deployments/:id/backups backups:write

Start a manual backup of a deployment.

id:path
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/backups" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "ok": true,
  "backupId": "bak_a1b2c3d4"
}
GET /backups/:backupId backups:read

Fetch a single backup by id.

backupId:path
curl "https://bot-hosting.net/api/v1/backups/bak_a1b2c3d4" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "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 /backups/:backupId backups:write destructive

Delete a backup.

backupId:path
curl -X DELETE "https://bot-hosting.net/api/v1/backups/bak_a1b2c3d4" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "ok": true
}
POST /backups/:backupId/restore backups:write destructive

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

backupId:pathdeploymentId:bodystartAfter:body?
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}'
Response 200
{
  "ok": true,
  "warning": "string"
}
GET /deployments/:id/packages packages:read

List packages in the manifest (npm or pip).

id:pathmanager:query
manager
One of: npm, pip.
curl "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/packages?manager=npm" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "manager": "pip",
  "file": "archive.zip",
  "exists": true,
  "packages": [
    {
      "name": "my-bot",
      "spec": "==1.0.0",
      "dev": true
    }
  ]
}
POST /deployments/:id/packages packages:write

Add or update a package in the manifest.

id:pathmanager:bodyname:bodyspec:body?dev:body?
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}'
Response 200
{
  "manager": "pip",
  "file": "archive.zip",
  "exists": true,
  "packages": [
    {
      "name": "my-bot",
      "spec": "==1.0.0",
      "dev": true
    }
  ]
}
POST /deployments/:id/packages/remove packages:write

Remove a package from the manifest.

id:pathmanager:bodyname:body
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"}'
Response 200
{
  "manager": "pip",
  "file": "archive.zip",
  "exists": true,
  "packages": [
    {
      "name": "my-bot",
      "spec": "==1.0.0",
      "dev": true
    }
  ]
}
GET /projects projects:read

List the projects you own or collaborate on.

curl "https://bot-hosting.net/api/v1/projects" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "projects": [
    {
      "id": "dep_a1b2c3d4",
      "name": "my-bot",
      "description": "A cool Discord bot",
      "isOwner": true,
      "createdAt": "2026-01-01T00:00:00.000Z"
    }
  ]
}
POST /projects projects:write

Create a project.

name:bodydescription:body?
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"}'
Response 200
{
  "id": "dep_a1b2c3d4",
  "name": "my-bot",
  "description": "A cool Discord bot",
  "isOwner": true,
  "createdAt": "2026-01-01T00:00:00.000Z"
}
DELETE /projects/:id projects:write destructive

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

id:path
curl -X DELETE "https://bot-hosting.net/api/v1/projects/dep_a1b2c3d4" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "ok": true
}
POST /deployments/:id/domains deployments:write

Assign a subdomain (activate domains). Idempotent.

id:path
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "subdomain": "my-bot.apps",
  "url": "https://.../files/download?path=%2Fmain.py",
  "note": "string"
}
PATCH /deployments/:id/domains/slug deployments:write

Set the subdomain alias (slug).

id:pathslug:body
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"}'
Response 200
{
  "ok": true,
  "url": "https://.../files/download?path=%2Fmain.py"
}
DELETE /deployments/:id/domains/slug deployments:write

Remove the subdomain alias.

id:path
curl -X DELETE "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains/slug" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "ok": true
}
PATCH /deployments/:id/domains/custom deployments:write

Attach a custom domain. Returns the DNS verification token.

id:pathdomain:body
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"}'
Response 200
{
  "token": "string"
}
POST /deployments/:id/domains/custom/verify deployments:write

Check the DNS for the attached custom domain.

id:path
curl -X POST "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains/custom/verify" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "verified": true,
  "reason": "string"
}
DELETE /deployments/:id/domains/custom deployments:write

Detach the custom domain.

id:path
curl -X DELETE "https://bot-hosting.net/api/v1/deployments/dep_a1b2c3d4/domains/custom" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "ok": true
}
GET /deployments/:id/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.

id:pathpath:query?method:query?body:query?headers:query?
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"
Response 200
{
  "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"
}
GET /docs/:slug docs:read

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

slug:path
curl "https://bot-hosting.net/api/v1/docs/my-template" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "slug": "my-template",
  "title": "string",
  "section": "string",
  "url": "https://.../files/download?path=%2Fmain.py",
  "content": "print(\"hello world\")"
}
GET /deployments/:id/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.

id:pathpath:query?
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"
Response 200
{
  "files": [
    {
      "path": "/main.py",
      "lines": 1,
      "symbols": [
        {
          "kind": "function",
          "name": "my-bot",
          "line": 1
        }
      ]
    }
  ],
  "truncated": true,
  "note": "string"
}

Account

GET /account account:read

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

curl "https://bot-hosting.net/api/v1/account" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "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

GET /templates templates:read

Browse the public template catalogue (any author).

sort:query?category:query?q:query?page:query?perPage:query?
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"
Response 200
{
  "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"
    }
  ]
}
GET /templates/:slug templates:read

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

slug:path
curl "https://bot-hosting.net/api/v1/templates/my-template" \
  -H "Authorization: Bearer bhk_your_key"
Response 200
{
  "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"
}