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
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
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
Errors & conventions
| Code | Meaning |
|---|---|
| 400 | bad input |
| 401 | missing/invalid api_key |
| 402 | needs credit |
| 403 | limit (anon IP server cap, never_sleep without verified phone, domain) |
| 404 | server not found / not yours |
| 409 | subdomain taken |
| 429 | rate limit |
| 503 | at 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:
| field | type | what it does |
|---|---|---|
subdomains | string[] | 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_sleep | bool | Keep 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. |
phone | string | Sign up / log in with a phone (E.164) → verified account, no IP limit. Two-step (below). |
code | string | The 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":"…" }
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.phone, or verify a phone, to lift it.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" }
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: apex → A to our edge IP · www.mydomain.com → CNAME to your apex (mydomain.com), which already points at us · any other subdomain → CNAME 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" }
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 }
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", … }
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_…", … }
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).
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" }
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