// the api

API reference

Deploy a real root Linux server with one HTTP call. Plain REST, curl-able, no MCP install. Base URL https://thevibehosting.com · JSON in/out. Agents: read SKILL.md first.

Auth

The user layer is an account; the credential is an api_key. The first POST /create (no auth) creates your account and returns the key. Send it on every other call:

Authorization: Bearer vibe_xxxxxxxx
Anonymous ≠ no identity — it's an account with no verified phone; the api_key is still your credential. Phone verification just attaches a phone to the same account; the key doesn't change. Auth-required endpoints return 401 without a token; only /create and /feedback work anonymously.

Deploy your app

It's a full root Linux box — SSH in and run whatever you want (web app, worker, bot, cron, a database, compile things). Nothing forces a particular layout. The options below are just the convenient ways to put a web app on your public HTTPS subdomain; if you don't serve HTTP, ignore them.

Your subdomain routes to port 80 in the container. To publish there:

1 — Static: drop files in /root/www (served immediately, nothing to kill).   2 — Your own server: point our supervisor at it with an executable /root/run.sh bound to 0.0.0.0:80 — it gets auto-restarted and survives restarts. Then free :80 from the default placeholder:

# optional — only if you want YOUR long-running server on the public :80
cat > /root/run.sh <<'SH'
#!/usr/bin/env bash
cd /root/myapp
exec python3 server.py        # must listen on 0.0.0.0:80
SH
chmod +x /root/run.sh
pkill -f busybox              # drop the default placeholder; container stays up
Keep it light (target RAM < 128 MB). Per-server RAM ceiling: 128 MB (no phone) / 512 MB (verified). Disk quota: 256 MB (anon) / 1 GB (verified), always-on scales with the plan's RAM — exceeding it suspends the server. Don't add keep-alive pings — idle servers sleep = free. Need a non-HTTP TCP port? Use /expose.

Errors & conventions

CodeMeaning
400bad input
401missing/invalid api_key
402always-on plan needs credit
403limit (anon IP server cap, always_on, domain)
404server not found / not yours
409subdomain taken
429rate limit
503at capacity / not configured

When the free grant is exhausted and there's no credit, read endpoints return an upgrade envelope:

{ "upgrade_required": true, "reason": "free_tier_exceeded",
  "upgrade_url": "https://thevibehosting.com/account/recharge",
  "message": "Server will be paused in 48h. Top up credit, or verify a phone…" }

Objects

// Credits
{ "tier":"anonymous", "ram_gb_hours_left":100.0, "egress_gb_left":5.0,
  "disk_mb_left":512.0, "balance_usd":0.0 }

// Server
{ "id":"srv_…", "subdomain":"abc123.thevibehosting.com", "state":"running",
  "always_on":false, "plan":null, "suspended":false, "flag":null,
  "exposed_ports":[{"public":"46.225.188.115:40860","container_port":5432,"protocol":"tcp"}],
  "usage":{"disk_mb_used":0.0,"domains":[]}, "credits":{…} }

Endpoints

POST /create no auth → creates account

Create a root Linux server on an HTTPS subdomain. Auth optional — the first call (no token) creates your account and returns its api_key; send Authorization: Bearer <api_key> to create under an existing account. The body is optional — a bare curl -sX POST .../create is a complete call. All fields are optional:

fieldtypewhat it does
subdomainsstring[]Preference-ordered desired slugs; first available wins (include fallbacks). Lowercased, a–z 0–9 -, 3–32 chars; reserved/brand labels skipped. Omit for a random slug. None usable/free → random slug + subdomain_note.
planstringstandard 1 GB / pro 2 GB / max 4 GB ($3/$5/$9 mo). Implies always-on. Verified phone + credit.
always_onboolReserve RAM, never sleep (no cold starts). Uses standard unless plan set. Verified phone + credit.
phonestringSign up / log in with a phone (E.164) → verified account, no IP limit. Two-step (below).
codestringThe SMS code, to finish phone signup/login.

Phone signup or login (proves number ownership, two steps): (1) {"phone":"+420…"} → texts a code, replies verification_required (no server yet; providing the number is consent, every SMS has a STOP opt-out); (2) resend the same body + "code":"123456" → server on a verified account. Existing number → you're logged into it (its api_key returned); else a new verified account. account_note says which. The code is required — never a token/verified tier on an unproven number.

# simplest — anonymous, random slug
curl -sX POST https://thevibehosting.com/create
# choose a name (first free wins)
curl -sX POST https://thevibehosting.com/create -H 'content-type: application/json' \
  -d '{"subdomains":["caffeine-tracker","caffeine-app","caffeine"]}'
# 200
{ "id":"srv_ab12cd34ef",
  "ssh":"ssh root@46.225.188.115 -p 28210",
  "ssh_private_key":"-----BEGIN OPENSSH PRIVATE KEY-----\n…",
  "subdomain":"caffeine-tracker.thevibehosting.com",
  "subdomain_note":null, "account_note":null,
  "api_key":"vibe_0123…", "credits":{…}, "ONBOARDING":"…" }
Save ssh_private_key (chmod 600, shown only here) and api_key. SSH is ready within ~1–3 s, over IP:port (the subdomain is HTTP only). With a phone (no code yet) you instead get verification_required.
Server limits: verified = unlimited servers (one shared credit balance + grant, 512 MB RAM / 1 GB disk). Anonymous = 2 servers per creator IP (128 MB RAM / 256 MB disk each) — pass a phone, or verify a phone, to lift it.
Errors: 400 invalid phone / bad code · 403 anon IP limit, or always_on/plan without verified+funded · 429 rate limit · 503 at capacity.

GET /account

Who this api_key is.

{ "account_id":"acc_…","tier":"anonymous","phone_verified":false,"projects":1,"credits":{…} }

GET /servers

Array of your Server objects. curl -s …/servers -H "Authorization: Bearer vibe_…"

GET /servers/{id}

One server's status / usage / credits (incl. ssh + url). 404 if not yours.

GET /servers/{id}/ssh

{ "ssh":"ssh root@46.225.188.115 -p 28210", "user":"root",
  "host":"46.225.188.115", "port":28210, "subdomain":"myapp.thevibehosting.com" }
The private key is shown only once at create and is not stored — use the ssh_private_key you saved (chmod 600, ssh -i <key> …). Lost it? Append a new public key via /ssh-key. Connecting also wakes a sleeping server.

GET /servers/{id}/usage

{ "server":"srv_…", "credits":{…}, "disk_mb_used":0.0 }

Returns the upgrade envelope instead if the grant is exhausted.

POST /servers/{id}/subdomain

{ "subdomain":"newslug" }{ "subdomain":"newslug.thevibehosting.com", "restarted":true, "warning":"…" }. 409 if taken. Re-points routing with a brief restart; open SSH drops — reconnect (host key unchanged).

POST /servers/{id}/domain verified tier

{ "domain":"example.com" } → CNAME instructions + verification status.

POST /servers/{id}/expose

Publish a raw TCP/UDP port (for non-HTTP services; HTTP on :80 is already your subdomain); max 5 ports/server. A TCP port becomes reachable within a few seconds with no restart (and wakes the server on connect); a UDP port needs a brief restart to publish (open SSH drops).

# request  { "container_port":5432, "protocol":"tcp" }
# 200      { "public":"46.225.188.115:40860", "container_port":5432, "protocol":"tcp" }

POST /servers/{id}/ssh-key

{ "public_key":"ssh-ed25519 AAAA…" }{ "ok":true }

POST /servers/{id}/sleep

Scale to zero now — stop the container, free RAM, pause RAM billing. Don't wait for the 10-min idle timeout when you're done for a while. Everything persists; it wakes on the next HTTP request / SSH connection, or /wake. → { "state":"sleeping" }

409 if suspended, or if it's an always-on server (those keep reserved RAM and never sleep).

POST /servers/{id}/wake

Start a sleeping server (pre-warm). Blocks briefly until the app binds :80 so you know it's ready; a non-HTTP server just reports started. → { "state":"running", "http_ready":true }

Servers wake automatically on any inbound HTTP request or SSH connection — use this only to pre-warm or to wake without hitting the app.

DELETE /servers/{id}

Delete the server and its data. → { "ok":true }

POST /account/verify-phone

Attach a verified phone → upgrades to the verified tier (unlimited servers on one shared balance, custom domains, always-on, bigger grant). Agent-driven, two steps.

# step 1 — show the returned disclosure first, then send the code (needs consent)
{ "phone":"+420600000000", "consent":true }   → { "sent":true, … }
# step 2 — confirm the SMS code
{ "phone":"+420600000000", "code":"123456" }   → { "verified":true, "tier":"verified", … }
The number is never resold or shared, used only for hosting notifications, with an opt-out link in every SMS.

POST /account/recover

Lost your api_key? Recover it by re-verifying the phone on the account. No auth needed; two steps.

# step 1 — send a code to the phone on the account
{ "phone":"+420600000000" }              → { "sent":true, … }
# step 2 — confirm; returns your api_key
{ "phone":"+420600000000", "code":"123456" } → { "recovered":true, "api_key":"vibe_…", … }
Only works if a phone was verified on the account — another reason to verify one.

POST /account/recharge verified phone alias: /account/topup

Add credit in advance, any time, via Stripe Checkout. { "amount_usd":5 }{ "checkout_url":"https://checkout.stripe.com/…" }. Open the URL to pay; balance is credited automatically (min $5).

Requires a verified phone. Until you verify one, recharge returns 403 — verify your phone first with POST /account/verify-phone. This keeps anonymous accounts from adding credit.

POST /feedback no auth ok

Tell us what to add or fix — we read every one. Add email for a reply.

{ "kind":"feature", "message":"add a /logs endpoint", "email":"you@x.com" }
→ { "ticket_id":"fb_…", "thanks":"Recorded, thank you!" }
# kind: feature | bug | friction | limit | image_request | other