NebuliHost API

Manage your servers and their files from scripts, CI pipelines and other tools.

Getting started

All endpoints live under this base URL and return JSON, except file downloads.

https://hosting.thehsi.cloud/api/v1

Create a key in Settings → API Keys, then check that it works:

curl https://hosting.thehsi.cloud/api/v1/me \
  -H "Authorization: Bearer $NEBULI_API_KEY"

Authentication

Send your key in the Authorization header of every request:

Authorization: Bearer nh_…
A key acts as your account. Keep it out of source code, store it as a secret in your CI, and revoke it in Settings straight away if it leaks. The full key is only shown once, when you create it.

Keys belong to your primary account. If you have linked accounts, the API works with the same servers you see in the dashboard. You can have up to 10 keys.

Permissions

Each key only gets the permissions you select for it. Give a key only what it needs. For example, a deploy script needs files:write but not servers:write.

PermissionAllows
account:readRead your account and plan
servers:readList servers and view their details
servers:writeCreate and delete servers
files:readList and download files
files:writeUpload, create, rename and delete files

Your plan's limits still apply: a key can't create more servers or upload more data than your plan allows, and actions your plan doesn't include (for example uploading files) fail with missing_privilege. The API only works with servers you own. Admin privileges don't apply to API keys.

Errors

Failed requests use a normal HTTP status code and a JSON body with a stable error code you can check in your code, plus a readable message:

{
  "success": false,
  "error": "server_not_found",
  "message": "Server not found"
}
StatusError codesMeaning
400invalid_json invalid_id invalid_path is_directory …The request is malformed or a value is invalid.
401missing_api_key invalid_api_keyNo key was sent, or the key is wrong or revoked.
403insufficient_scope missing_privilege forbidden server_limit_reached account_disabledThe key or your plan doesn't allow this, or the server isn't yours.
404server_not_found not_foundThe server, file or endpoint doesn't exist.
409server_exists already_exists is_fileSomething with that name already exists.
411length_requiredRaw uploads need a Content-Length header.
413storage_limit_exceededThe upload would exceed the server's storage limit.
500io_errorSomething went wrong on our side. Try again later.

Account

GET/api/v1/me account:read

Your account, plan and limits. max_servers is -1 when unlimited, and max_server_size (bytes) is null when unlimited.

{
  "success": true,
  "account": {
    "id": 42,
    "username": "jane.doe",
    "email": "jane@example.com",
    "provider": "github",
    "plan": "premium",
    "max_servers": 5,
    "server_count": 2,
    "max_server_size": 1073741824,
    "linked_providers": ["google"]
  }
}

Servers

A server is one website, served at https://<id>.thehsi.cloud. Server objects look like this (size and max_size are in bytes, and max_size is null when unlimited):

{
  "id": "my-site",
  "url": "https://my-site.thehsi.cloud",
  "status": "running",
  "size": 48213,
  "max_size": 1073741824,
  "created_at": "2026-11-03T09:12:44+00:00"
}
GET/api/v1/servers servers:read

All your servers, as {"success": true, "servers": [ … ]}.

POST/api/v1/servers servers:writeplan: create_server

Creates a server with a default index.html. The id becomes the subdomain: 5 to 63 characters, lowercase letters, numbers and dashes, and it can't start or end with a dash. Returns 201 with the new server.

curl -X POST https://hosting.thehsi.cloud/api/v1/servers \
  -H "Authorization: Bearer $NEBULI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"id": "my-site"}'
GET/api/v1/servers/{id} servers:read

One server, as {"success": true, "server": { … }}.

DELETE/api/v1/servers/{id} servers:writeplan: delete_server
This permanently deletes the server and all its files. It can't be undone.

Files

File paths are relative to the server root, for example index.html or assets/app.css. URL-encode special characters in paths. Paths that point outside the server are rejected.

GET/api/v1/servers/{id}/files files:read

The full file tree. Folders are objects, and files map to their size in bytes.

{
  "success": true,
  "files": {
    "assets": { "app.css": 2310, "logo.svg": 911 },
    "index.html": 1204
  }
}
GET/api/v1/servers/{id}/files/{path} files:readplan: download_file

Downloads a file. If path is a folder, you get a zip of that folder.

curl -OJ https://hosting.thehsi.cloud/api/v1/servers/my-site/files/index.html \
  -H "Authorization: Bearer $NEBULI_API_KEY"
GET/api/v1/servers/{id}/archive files:readplan: download_file

A zip of the whole server, useful for backups.

curl -o my-site.zip https://hosting.thehsi.cloud/api/v1/servers/my-site/archive \
  -H "Authorization: Bearer $NEBULI_API_KEY"
PUT/api/v1/servers/{id}/archive files:writeplan: upload_file

Unpacks a zip into the server, as a multipart upload with a file field and a required mode field. Use merge to add the zip's files and overwrite those with the same path, or replace to delete everything else on the server (this also needs plan: manage_files).

The zip is checked and unpacked before the site changes, so a rejected zip leaves it untouched. If everything is inside one folder that has an index.html, that folder's contents go to the server root, and the response names it in stripped_folder. Password-protected zips and symbolic links are rejected.

curl -X PUT https://hosting.thehsi.cloud/api/v1/servers/my-site/archive \
  -H "Authorization: Bearer $NEBULI_API_KEY" \
  -F "file=@my-site.zip" \
  -F "mode=replace"
{
  "success": true,
  "mode": "replace",
  "files": 42,
  "stripped_folder": null
}
PUT/api/v1/servers/{id}/files/{path} files:writeplan: upload_file

Creates or replaces a file. Missing parent folders are created. Send the file as the raw request body (with Content-Length), or as a multipart form field named file. The write is atomic: visitors never see a half-uploaded file.

# Raw body
curl -X PUT https://hosting.thehsi.cloud/api/v1/servers/my-site/files/index.html \
  -H "Authorization: Bearer $NEBULI_API_KEY" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @index.html

# Multipart
curl -X PUT https://hosting.thehsi.cloud/api/v1/servers/my-site/files/assets/logo.svg \
  -H "Authorization: Bearer $NEBULI_API_KEY" \
  -F "file=@logo.svg"
POST/api/v1/servers/{id}/directories/{path} files:writeplan: upload_file

Creates a folder, including any missing parent folders. Succeeds if the folder already exists.

PATCH/api/v1/servers/{id}/files/{path} files:writeplan: manage_files

Renames or moves a file or folder. Fails with already_exists if the destination is taken.

curl -X PATCH https://hosting.thehsi.cloud/api/v1/servers/my-site/files/old.html \
  -H "Authorization: Bearer $NEBULI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"new_path": "archive/old.html"}'
DELETE/api/v1/servers/{id}/files/{path} files:writeplan: manage_files

Deletes a file, or a folder with everything in it.

Deploy from CI

This script uploads everything in a build folder to a server. Create a key with only files:write, and store it as the NEBULI_API_KEY secret in your CI.

#!/usr/bin/env bash
set -euo pipefail

API="https://hosting.thehsi.cloud/api/v1"
SERVER="my-site"
BUILD_DIR="dist"

cd "$BUILD_DIR"
find . -type f -print0 | while IFS= read -r -d '' file; do
  path="${file#./}"
  echo "Uploading $path"
  curl --fail-with-body -sS -X PUT "$API/servers/$SERVER/files/$path" \
    -H "Authorization: Bearer $NEBULI_API_KEY" \
    -H "Content-Type: application/octet-stream" \
    --data-binary "@$file" > /dev/null
done

Files that are no longer in your build stay on the server. To remove them, compare the tree from GET /files with your build folder and delete the extras, or zip the folder and upload it with mode=replace.