API documentation
Your agent can host itself
The whole platform is an email address and a REST API away: register, top up with crypto, deploy a fleet, destroy it when done. No human in the loop required - this page tells your agent everything it needs.
1. Point it at one URL
Give your agent /llms.txt - a plain-text version of this page that LLM tools fetch and follow out of the box.
2. Or paste the context
Copy the full instruction block below straight into your agent's system prompt or tool description. It is self-contained.
3. It does the rest
Register → check balance → deploy → poll until ACTIVE → SSH as root → destroy. Hourly billing means experiments cost cents.
# NoctHost - API guide for AI agents
> NoctHost is a no-KYC, crypto-funded layer over top-tier clouds: it provisions
> REAL cloud servers (NVMe, dedicated IPv4, root SSH) on Vultr and DigitalOcean
> today — Hetzner and Linode next — via their APIs, billed by the hour. It is NOT a
> reseller or a Proxmox host. An email is the whole identity. REST API over JSON.
>
> Base URL: https://api.nocthost.com
> Human docs: /docs · This file: /llms.txt
## Happy path (deploy one server)
1. POST /auth/register {"email":"...","password":"min 8 chars"}
-> 201 {"accessToken":"<jwt>","user":{...}} (or POST /auth/login if registered)
2. Send "Authorization: Bearer <accessToken>" on every request below.
For scripts, create a non-expiring API key in the dashboard (Profile ->
API keys) and send it the same way: "Authorization: Bearer nh_...".
3. GET /billing -> {"balance":"10.00","hourlySpend":"0"}
Balance must cover >= 1 hour of the chosen plan, else POST /servers returns 402.
4. POST /servers {"label":"my-bot","planId":"vc2-1c-1gb","regionId":"ams","password":"<root pw>"}
-> 202 {"server":{"id":"...","status":"PROVISIONING","ipv4":null,...}}
The password sets the server's root login; it is never stored, so keep it.
5. Poll GET /servers/<id> every ~5s until "status":"ACTIVE" (typically ~60s).
Then "ipv4" is set - SSH in as root with the password you chose.
6. DELETE /servers/<id> -> 202. Billing for it stops the same hour.
## Endpoints
POST /auth/register
Create an account. Email + password is all that is needed.
body: {"email":"[email protected]","password":"min-8-chars"}
returns: 201 {"accessToken":"<jwt>","user":{"id","email","balance"}}
POST /auth/login
Get a fresh access token (expires in 15 minutes).
body: {"email":"...","password":"..."}
returns: 200 {"accessToken":"<jwt>","user":{...}}
GET /auth/me (auth)
Whoami: id, email and current balance.
returns: 200 {"user":{"id","email","balance":"9.96"}}
GET /billing (auth)
Balance and total hourly spend across active servers.
returns: 200 {"balance":"9.96","hourlySpend":"0.012"}
GET /servers (auth)
List all non-destroyed servers.
returns: 200 {"servers":[{"id","label","status","region","plan","ipv4","hourlyPrice","createdAt"}]}
GET /servers/:id (auth)
One server. Poll this every ~5s after a deploy.
returns: 200 {"server":{...,"status":"ACTIVE","ipv4":"203.0.113.10"}}
POST /servers (auth)
Deploy one server. Async: 202 = accepted, not yet running. Optional osId (Vultr OS id, default Ubuntu 22.04) or appSlug (one-click app) — pick at most one. osId/appSlug apply to Vultr only; DigitalOcean deploys use the default OS, so omit them for providerId 'digitalocean'.
body: {"label":"my-bot","planId":"vc2-1c-1gb","regionId":"ams","osId":1743,"password":"12-72 chars, no spaces"}
returns: 202 {"server":{...,"status":"PROVISIONING"}}
POST /servers/bulk (auth)
Deploy up to 10 servers in one call. Labels get -1..-N suffixes.
body: {"label":"fleet","count":10,"planId":"vc2-1c-1gb","regionId":"ams","password":"applied to all"}
returns: 202 {"servers":[...10 items, all "PROVISIONING"]}
POST /servers/:id/reboot (auth)
Reboot a running server.
returns: 200 {"server":{...}}
POST /servers/:id/stop (auth)
Power off a server. STOPPED is still billed while it exists.
returns: 200 {"server":{...,"status":"STOPPED"}}
POST /servers/:id/start (auth)
Power a STOPPED server back on.
returns: 200 {"server":{...,"status":"ACTIVE"}}
DELETE /servers/:id (auth)
Destroy a server. Billing stops the same hour.
returns: 202 {"ok":true}
GET /backups (auth)
List your backups (full disk snapshots, kept 7 days).
returns: 200 {"backups":[{"id","label","status","region","plan","createdAt","expiresAt"}]}
POST /backups (auth)
Back up a server. Async: snapshot takes minutes, PENDING -> AVAILABLE.
body: {"serverId":"<id>"}
returns: 202 {"backup":{...,"status":"PENDING"}}
POST /backups/:id/restore (auth)
Restore an AVAILABLE backup onto a new server (gets a new IP).
returns: 202 {"server":{...,"status":"PROVISIONING"}}
DELETE /backups/:id (auth)
Delete a backup before its 7-day expiry.
returns: 202 {"ok":true}
## Catalog
plans: vc2-1c-1gb Micro (1 vCPU / 1 GB / 25 GB, $10/mo) · vc2-1c-2gb Starter (1 / 2 / 55, $18) · vc2-2c-4gb Standard (2 / 4 / 80, $34) · vc2-4c-8gb Pro (4 / 8 / 160, $62) · vc2-6c-16gb Beast (6 / 16 / 320, $120)
regions: ams fra cdg lhr mad waw sto · ewr ord dfw atl mia sea lax sjc yto mex · sao scl · nrt icn sgp blr bom del · syd mel · jnb tlv
Hourly price is returned on the server object at deploy time and never changes
for a running server.
## Server statuses
PROVISIONING -> ACTIVE (success) | ERROR (failed, not billed further)
ACTIVE <-> STOPPED (powered off via /servers/:id/stop or /start; STOPPED is still billed while it exists)
DESTROYING -> DESTROYED
SUSPENDED = balance ran out or abuse hold; server is off but recoverable.
ARCHIVED = balance ran out; the instance is deleted but your data is kept in a snapshot — top up to restore.
## Backups
On-demand backups are Vultr-only for now (DigitalOcean/Linode cannot be backed up on demand). Full disk snapshots, kept 7 days, then auto-deleted.
POST /backups {"serverId"} -> snapshot (PENDING -> AVAILABLE, takes minutes).
POST /backups/<id>/restore -> new server from the snapshot (new IP, same plan/region).
Backup statuses: PENDING -> AVAILABLE | ERROR; EXPIRED once deleted.
## Errors
400 validation failed ("details" in body) | 401 token expired -> re-login, retry once
402 insufficient balance -> top up | 403 suspended or server limit (default 10, lower during promos)
404 not yours / not found | 409 already being destroyed
429 rate limited (100 req/min, 10/min on /auth/*) -> exponential backoff
## Rules for agents
- Deploys are asynchronous. 202 means accepted; poll GET /servers/:id every ~5 seconds until status is ACTIVE (typically ~60s) or ERROR.
- For automation, prefer a personal API key over login: create one in the dashboard under Profile, then send it as "Authorization: Bearer nh_...". It does not expire, is revocable, and needs no re-login. Login-issued access tokens (JWT) still work but expire after 15 minutes.
- Access tokens (from POST /auth/login) expire after 15 minutes. On a 401 with a JWT, call POST /auth/login again and retry once. API keys (nh_...) do not expire - a 401 there means the key was revoked.
- Money values (balance, hourlyPrice, hourlySpend) are strings - parse as decimal, never float-math them.
- Check GET /billing before deploying: the balance must cover at least one hour per server, or you get 402.
- Server labels must match ^[a-zA-Z0-9._-]+$ (max 64 chars).
- providerId is optional and defaults to "vultr"; "digitalocean" is also available (Linux only, a subset of regions). Sending an unavailable provider or an unsupported region/plan/OS for it returns 400.
- The default account limit is 10 active servers (temporarily lower, e.g. 5, during a promo) and can be raised. 403 with a limit message means destroy something first or ask for a raise via /docs/custom-requests.
- Rate limit is 100 requests/minute (10/min on auth routes). On 429, back off exponentially.
- Always destroy servers you no longer need - billing is hourly and stops only at DELETE.
- On-demand backups (POST /backups) are Vultr-only for now — DigitalOcean/Linode servers cannot be backed up on demand (automatic snapshots on suspension still work on any provider). Backups are full disk snapshots kept 7 days. POST /backups is async (PENDING -> AVAILABLE, minutes); only restore once AVAILABLE. Restore creates a new server with a new IP.
Authentication
Every request marked auth carries a bearer token. There are two kinds, and they go in the same header.
API key — for scripts and agents
Create one in Profile → API keys. It starts with nh_, never expires, and keeps working while you sleep. Revoke it any time — anything using it stops immediately.
curl https://api.nocthost.com/servers \
-H "Authorization: Bearer $NOCTHOST_KEY"Access token — for interactive sessions
POST /auth/login returns a short-lived token (15 minutes). Fine for a one-off script you run by hand; for anything unattended use an API key instead.
curl -X POST https://api.nocthost.com/auth/login \
-H "content-type: application/json" \
-d '{"email":"[email protected]","password":"…"}'Admin endpoints are the exception: they require an access token and reject API keys.
Endpoint reference
JSON in, JSON out. Authorization: Bearer <accessToken> on everything marked auth.
/auth/registerCreate an account. Email + password is all that is needed.
body: {"email":"[email protected]","password":"min-8-chars"}
returns: 201 {"accessToken":"<jwt>","user":{"id","email","balance"}}
/auth/loginGet a fresh access token (expires in 15 minutes).
body: {"email":"...","password":"..."}
returns: 200 {"accessToken":"<jwt>","user":{...}}
/auth/meauthWhoami: id, email and current balance.
returns: 200 {"user":{"id","email","balance":"9.96"}}
/billingauthBalance and total hourly spend across active servers.
returns: 200 {"balance":"9.96","hourlySpend":"0.012"}
/serversauthList all non-destroyed servers.
returns: 200 {"servers":[{"id","label","status","region","plan","ipv4","hourlyPrice","createdAt"}]}
/servers/:idauthOne server. Poll this every ~5s after a deploy.
returns: 200 {"server":{...,"status":"ACTIVE","ipv4":"203.0.113.10"}}
/serversauthDeploy one server. Async: 202 = accepted, not yet running. Optional osId (Vultr OS id, default Ubuntu 22.04) or appSlug (one-click app) — pick at most one. osId/appSlug apply to Vultr only; DigitalOcean deploys use the default OS, so omit them for providerId 'digitalocean'.
body: {"label":"my-bot","planId":"vc2-1c-1gb","regionId":"ams","osId":1743,"password":"12-72 chars, no spaces"}
returns: 202 {"server":{...,"status":"PROVISIONING"}}
/servers/bulkauthDeploy up to 10 servers in one call. Labels get -1..-N suffixes.
body: {"label":"fleet","count":10,"planId":"vc2-1c-1gb","regionId":"ams","password":"applied to all"}
returns: 202 {"servers":[...10 items, all "PROVISIONING"]}
/servers/:id/rebootauthReboot a running server.
returns: 200 {"server":{...}}
/servers/:id/stopauthPower off a server. STOPPED is still billed while it exists.
returns: 200 {"server":{...,"status":"STOPPED"}}
/servers/:id/startauthPower a STOPPED server back on.
returns: 200 {"server":{...,"status":"ACTIVE"}}
/servers/:idauthDestroy a server. Billing stops the same hour.
returns: 202 {"ok":true}
/backupsauthList your backups (full disk snapshots, kept 7 days).
returns: 200 {"backups":[{"id","label","status","region","plan","createdAt","expiresAt"}]}
/backupsauthBack up a server. Async: snapshot takes minutes, PENDING -> AVAILABLE.
body: {"serverId":"<id>"}
returns: 202 {"backup":{...,"status":"PENDING"}}
/backups/:id/restoreauthRestore an AVAILABLE backup onto a new server (gets a new IP).
returns: 202 {"server":{...,"status":"PROVISIONING"}}
/backups/:idauthDelete a backup before its 7-day expiry.
returns: 202 {"ok":true}
Catalog IDs
Plans: vc2-1c-1gb Micro (1 vCPU / 1 GB / 25 GB, $10/mo) · vc2-1c-2gb Starter (1 / 2 / 55, $18) · vc2-2c-4gb Standard (2 / 4 / 80, $34) · vc2-4c-8gb Pro (4 / 8 / 160, $62) · vc2-6c-16gb Beast (6 / 16 / 320, $120)
Regions: ams fra cdg lhr mad waw sto · ewr ord dfw atl mia sea lax sjc yto mex · sao scl · nrt icn sgp blr bom del · syd mel · jnb tlv
Rules your agent should follow
- ▸Deploys are asynchronous. 202 means accepted; poll GET /servers/:id every ~5 seconds until status is ACTIVE (typically ~60s) or ERROR.
- ▸For automation, prefer a personal API key over login: create one in the dashboard under Profile, then send it as "Authorization: Bearer nh_...". It does not expire, is revocable, and needs no re-login. Login-issued access tokens (JWT) still work but expire after 15 minutes.
- ▸Access tokens (from POST /auth/login) expire after 15 minutes. On a 401 with a JWT, call POST /auth/login again and retry once. API keys (nh_...) do not expire - a 401 there means the key was revoked.
- ▸Money values (balance, hourlyPrice, hourlySpend) are strings - parse as decimal, never float-math them.
- ▸Check GET /billing before deploying: the balance must cover at least one hour per server, or you get 402.
- ▸Server labels must match ^[a-zA-Z0-9._-]+$ (max 64 chars).
- ▸providerId is optional and defaults to "vultr"; "digitalocean" is also available (Linux only, a subset of regions). Sending an unavailable provider or an unsupported region/plan/OS for it returns 400.
- ▸The default account limit is 10 active servers (temporarily lower, e.g. 5, during a promo) and can be raised. 403 with a limit message means destroy something first or ask for a raise via /docs/custom-requests.
- ▸Rate limit is 100 requests/minute (10/min on auth routes). On 429, back off exponentially.
- ▸Always destroy servers you no longer need - billing is hourly and stops only at DELETE.
- ▸On-demand backups (POST /backups) are Vultr-only for now — DigitalOcean/Linode servers cannot be backed up on demand (automatic snapshots on suspension still work on any provider). Backups are full disk snapshots kept 7 days. POST /backups is async (PENDING -> AVAILABLE, minutes); only restore once AVAILABLE. Restore creates a new server with a new IP.
Servers for humans work here too
The same API powers the dashboard. Try it by hand first - deploy, poke around, destroy. Cents per hour.