API reference — The Vibe Hosting
// 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 — just write it; the supervisor switches over within ~1s on its own (no kill needed), auto-restarts it, and survives restarts:

# 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         # done — placeholder is swapped for your app automatically
You pay only for RAM used while awake ($0.005/GB-hour) — a 1 GB box 24/7 ≈ $1.35/mo after the free credit. Per-server RAM ceiling: 256 MB (no phone) / 2 GB (verified) / 4 GB once you hold paid credit. Disk: $0.25/GB-month on real use, ceiling 256 MB (anon) / 1 GB (verified) / 5 GB with credit. 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
402needs credit
403limit (anon IP server cap, never_sleep without verified phone, 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",
  "never_sleep":false, "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.
never_sleepboolKeep the server always running — never auto-scale-to-zero (no cold starts). Billed by real RAM 24/7 from your credit, same rate as any awake server. Verified phone.
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, 2 GB RAM — 4 GB once you hold paid credit, 1 GB disk). Anonymous = 2 servers per creator IP (256 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 never_sleep without a verified phone · 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

Serve your app from your own domain — apex (mydomain.com) and/or www. — HTTPS auto-issued, URL stays on your domain (no redirect). Verified accounts only (403 otherwise). The response is self-describing: the exact dns records to set plus instructions. ⚠️ One POST per hostname — register each hostname separately, including www. Nothing is auto-added: the apex does not pull in www, so to serve both mydomain.com and www.mydomain.com you make two calls (the apex response carries a warning).

DNS records by type: apexA to our edge IP · www.mydomain.comCNAME to your apex (mydomain.com), which already points at us · any other subdomainCNAME to your <slug>.thevibehosting.com.

# request  { "domain":"mydomain.com" }
# 200      { "domain":"mydomain.com", "status":"pending", "url":"https://mydomain.com",
           "dns":[ { "type":"A", "name":"mydomain.com", "value":"46.225.188.115" } ],
           "warning":"www.mydomain.com is NOT served yet — register it separately: POST …/domain {\"domain\":\"www.mydomain.com\"}, then CNAME www → mydomain.com",
           "instructions":"… set the records, then GET /servers/{id}/domain to poll …" }

You don't have to poll — a background reconciler activates pending domains once DNS points at us (a not-yet-pointing domain reports observed_ips vs the expected edge IP). GET /servers/{id}/domain lists domains + status; DELETE /servers/{id}/domain {domain} detaches one.

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 a never_sleep server (those stay running, billed by real RAM 24/7).

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.

POST /servers/{id}/refresh-ram

Apply your account's entitled RAM ceiling to this server (256 MB anon / 2 GB verified / 4 GB while you hold paid credit ≥ $1) — call it after topping up to claim the bigger box. Data preserved; a running server gets a brief restart (SSH drops, reconnect). → { "ram_ceiling_mb":4096, "old_mb":2048, "changed":true, "restarted":true } (changed:false if already at it).

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, never-sleep, bigger free credit + 2–4 GB RAM ceiling, custom domains). 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.

GET /account/billing

Usage-based billing snapshot: the two credit buckets, the monthly free floor + when it renews, the rates, your RAM ceiling, and your live burn rate.

{ "tier":"verified", "free_balance_usd":2.10, "paid_balance_usd":4.00,
  "free_credits_monthly":2.25, "credit_renews_at":"2026-07-18T00:00:00Z",
  "rate_ram_gb_hour":0.005, "rate_egress_gb":0.10, "rate_disk_gb_month":0.25,
  "ram_ceiling_mb":4096, "disk_ceiling_mb":5120, "disk_gb":0.4, "ceiling_boost_active":true,
  "now_burning":{ "ram_gb":0.75, "disk_gb":0.4, "usd_per_hour":0.00389, "servers_awake":2 },
  "runway_estimate":"≈ 2 months at the current burn" }
Free credit tops up to the floor on your founding-day anniversary; you spend free first, then paid. RAM is billed only while a server is awake. All timestamps are UTC, ISO 8601 with a trailing Z (e.g. 2026-07-18T00:00:00Z).

GET /account/usage

Hourly consumption history (one row per server per hour), newest first. ?limit=100&offset=0 (limit ≤ 500).

{ "count":1, "offset":0, "limit":100, "records":[
  { "server_id":"srv_…", "hour":"2026-06-18T10:00:00Z", "ram_gb_avg":0.5,
    "gb_hours":0.5, "egress_gb":0.0, "usd_charged":0.005, "paid_from":"free" } ] }

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