statichost.dev
API reference

statichost.dev API

statichost.dev hosts static websites. A project is one site: it gets a short random id (e.g. 29ufe) and is served at https://<id>.statichost.dev. You upload files into a project by path; they are live the moment the upload returns.

Base URLhttps://lno1g9bz.vibecode.cloud
Using an LLM?
Hand it /docs/llm/api.md β€” this whole reference as one Markdown file.
On this page

Authentication

Every call needs an API key, sent as a Bearer token:

Authorization: Bearer sk_...

The human creates the key at https://lno1g9bz.vibecode.cloud/account β†’ API keys (they must be signed in) and gives it to you. Treat it like a password. Keys act as their owner: everything the owner can do in the dashboard, the key can do.

Plans & limits

PlanSitesMax size per siteCustom domainsBadge
Free ($0)2100 MBno"Hosted on statichost.dev" footer
Pro ($19.99/mo, or $203.90/yr)101000 MByesnone

Going over a limit returns 403 site_limit or 413 site_too_large. Plans are changed by the human in the dashboard β€” the API cannot buy anything.

Publishing a site

  1. POST /api/v1/projects with {"name": "My site"} β†’ note the returned id and url. (To update an existing site instead, find it with GET /api/v1/projects.)
  2. For every file, PUT /api/v1/projects/{id}/files/{path} with the raw file bytes as the body. {path} is the file's path relative to the site root, e.g. index.html, css/site.css, img/logo.png (URL-encode each segment). Uploading the same path again replaces the file.
  3. Optional, when re-deploying: POST /api/v1/projects/{id}/prune with {"keep": [every path you just uploaded]} to delete files that no longer exist locally.
  4. The site is live at url. index.html (or home.html) in the root becomes the home page automatically; otherwise set homeFile with PATCH /api/v1/projects/{id}.

Shortcut: PUT /api/v1/projects/{id}/archive?replace=1 with a .zip as the body uploads the whole folder in one request (and, with replace=1, removes files not in the zip).

BASE=https://lno1g9bz.vibecode.cloud; KEY=sk_...
ID=$(curl -s -X POST $BASE/api/v1/projects -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' -d '{"name":"My site"}' | jq -r .id)
cd site && find . -type f | sed 's|^\./||' | while read -r f; do
  curl -s -X PUT --data-binary @"$f" -H "Authorization: Bearer $KEY" \
    "$BASE/api/v1/projects/$ID/files/$(jq -rn --arg p "$f" '$p|@uri|gsub("%2F";"/")')"
done

How sites are served

  • / serves the home file. /about tries about, then about.html, then about/index.html. A 404.html in the root is used for missing pages.
  • Root-absolute links (/css/site.css) work β€” each site owns its whole subdomain.
  • Content types come from the file extension.
  • Public files are cached at the CDN edge and purged automatically when anything about the site changes; browsers revalidate HTML on every visit. Password-protected sites, pages behind visitor login and pages with {{{workflow}}} tags are never cached.
  • Password protection (PATCH with {"password": "..."}) puts a statichost sign-in page in front of the whole site. {"password": null} removes it.
  • Custom domains (Pro): add the domain with POST /api/v1/projects/{id}/domains, then create CNAME <domain> β†’ <id>.statichost.dev (or, for an apex domain, TXT _statichost.<domain> = <id>). Free sites refuse to be served on a custom domain.

Dynamic pages (workflows)

Any HTML page can contain tags that are replaced, every time the page is served, by the result of a GluedEasy workflow:

<p>{{{wf_abc123 name="Ada"}}}</p>
<ul>{{{latest-posts @as=html @cache=300}}}</ul>
<p>Weather in your city: {{{weather/lookup city=$query.city @fallback="unavailable"}}}</p>
  1. The site owner connects GluedEasy once β€” dashboard Account β†’ Workflows (*Connect with AuthLock*: sign in and approve, no key to copy β€” or paste a key), or PUT /api/v1/integrations/gluedeasy with a key. Workflows run with that key: only workflows the owner can run, billed to the owner's GluedEasy credits. The key is stored encrypted and never reaches visitors.
  2. Upload pages with tags as usual. HTML files are scanned during upload; pages without tags are served exactly as before (streamed, cacheable).

Tag syntax: {{{<workflow>[/<step>] [name=value …] [@option=value …]}}}

  • <workflow> β€” a workflow id (wf_…) or its slug. /<step> picks the step to start at; without it the workflow's REST entry step is used. The run waits for the whole workflow and uses its final output.
  • name=value pairs become the workflow's JSON input. "quoted" values are strings; bare values are parsed as JSON when they can be (count=3 β†’ number, on=true), else strings; a bare name alone is true.
  • Visitor values (always strings): $query.<param> (from the page URL), $path, $host, $url, $lang (first Accept-Language). Different values are cached separately.
  • Options: @cache=<seconds> (default 60, 0 = run on every view, max 86400) Β· @timeout=<seconds> (default 10, max 30) Β· @as=text|html Β· @field=<a.b.c> (use one field of the output) Β· @fallback="<text>" (shown when the workflow can't run).

What is inserted: a string output as-is; an object's text field (or html with @as=html); anything else as JSON. With the default @as=text it is HTML-escaped β€” workflow output can never inject markup. @as=html allows formatting through an allowlist sanitizer (no scripts, styles, iframes, event handlers or javascript: URLs).

When it can't run (not connected, timeout, error, rate limit) the page is still served: the last good result (up to 1 h old) if there is one, else the @fallback, else nothing β€” plus an HTML comment such as <!-- statichost: my-flow not rendered (timeout) -->.

Limits: 20 tags per page, pages up to 10 MB, a site starts at most 120 workflow runs per minute (cache hits are free). Pages with tags are sent with Cache-Control: private, no-store. Tags work in .html files only, and results are never re-scanned for tags.

Visitor login

When the server has AuthLock configured, a site can get visitor login: turn it on with POST /api/v1/projects/{id}/access and statichost creates an AuthLock project for the site (the owner is invited to it as admin). Visitors sign in with an emailed code or a social account (Google, …) β€” no passwords β€” and may add an authenticator app (optional 2FA).

Access is set on the page tree: every directory ("" = the root, "docs/") and every .htm/.html file. A rule is one of

  • {"mode":"anonymous"} β€” open to everyone (the site password, if set, still applies)
  • {"mode":"signed_in"} β€” any visitor who signed in
  • {"mode":"roles","roles":["members","staff"]} β€” visitors with any of these roles
  • null β€” inherit

A page without its own rule uses its directory's, up to the root (default: anonymous). Every other file (CSS, images, …) follows its directory. Roles live in the site's AuthLock project; a rule that names a new role creates it. Assign roles to visitors with PUT /api/v1/projects/{id}/access/users/{userId}/roles (or in AuthLock).

On the site: a protected page shows a sign-in page (401) or "no access" (403); protected responses are never cached. Pages can link to /__statichost/auth/login?next=/path and /__statichost/auth/logout, and read the visitor with fetch('/__statichost/auth/me') β†’ { signedIn, email, roles }. Workflow tags can use $user.email, $user.id and $user.roles.

Errors

Errors are JSON: { "error": "<code>", "message": "<human text>" } with a matching HTTP status (400 invalid input, 401 missing/bad key, 403 not allowed by plan, 404 not found, 413 too large, 429 slow down, 5xx our problem β€” retry later).

502 storage_unavailable (with "retryable": true and a Retry-After header) means the file store had a transient hiccup and nothing was stored β€” wait a moment and repeat the exact same request. When uploading many files, retry each failed file (3–4 attempts with backoff) instead of giving up on the whole upload.

Account

GET/api/v1/me#

Who am I β€” the key owner, plan, limits and usage

Response
{
  "user": {
    "id": "k2j4…",
    "email": "you@example.com",
    "name": "You"
  },
  "plan": "free",
  "limits": {
    "sites": 2,
    "maxSiteBytes": 104857600,
    "customDomains": false,
    "badge": true
  },
  "usage": {
    "sites": 1
  }
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/me \
  -H "Authorization: Bearer $KEY"
GET/api/v1/integrations/gluedeasy#

Is a GluedEasy key connected (for {{{workflow}}} tags)?

Shows whether pages can run GluedEasy workflows and how they are connected (via: "key" = a pasted key, "authlock" = Connect with AuthLock). The key itself is never returned β€” only a hint.

Response
{
  "connected": true,
  "via": "key",
  "key": "sk_4f2…9a1c Β· you@example.com",
  "gluedeasyUrl": "https://gluedeasy.com"
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/integrations/gluedeasy \
  -H "Authorization: Bearer $KEY"
PUT/api/v1/integrations/gluedeasy#

Connect (or replace) your GluedEasy API key

The key is checked against GluedEasy first, then stored encrypted. Every {{{workflow}}} tag in your pages runs with it β€” see "Dynamic pages" above. Create the key in GluedEasy under Settings β†’ API Keys, with write access to the workflows your pages use.

Parameters

NameInTypeDescription
apiKey requiredbodystringA GluedEasy sk_… key.
Request body
{
  "apiKey": "sk_..."
}
Response
{
  "connected": true,
  "via": "key",
  "key": "sk_4f2…9a1c Β· you@example.com",
  "gluedeasyUrl": "https://gluedeasy.com"
}
curl
curl -s \
  -X PUT https://lno1g9bz.vibecode.cloud/api/v1/integrations/gluedeasy \
  -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' \
  -d '{"apiKey":"sk_..."}'

Errors: 400 invalid_gluedeasy_key 429 rate_limited

GET/api/v1/integrations/gluedeasy/workflows#

List the GluedEasy workflows your pages can use

Live, published workflows your connected GluedEasy key can run β€” the names to put in {{{…}}} tags. The editor autocompletes from this.

Response
{
  "workflows": [
    {
      "id": "wf_abc123",
      "slug": "opening-hours",
      "name": "Opening hours",
      "description": "Today's hours for the shop"
    }
  ]
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/integrations/gluedeasy/workflows \
  -H "Authorization: Bearer $KEY"

Errors: 409 gluedeasy_not_connected 400 gluedeasy_key_rejected 502 gluedeasy_unavailable

GET/api/v1/integrations/gluedeasy/workflows/{ref}#

One workflow: steps and the input names its code reads

{ref} is a workflow id (wf_…) or slug. inputs lists the argument names the entry step reads (input.x, destructured parameters, TypeScript input types) β€” inferred from the code, since GluedEasy has no input schema; use them as name=value in the tag.

Response
{
  "id": "wf_abc123",
  "slug": "opening-hours",
  "name": "Opening hours",
  "description": "…",
  "entryStep": "hours",
  "inputs": [
    "day",
    "shop"
  ],
  "steps": [
    {
      "id": "hours",
      "trigger": "rest",
      "inputs": [
        "day",
        "shop"
      ]
    }
  ]
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/integrations/gluedeasy/workflows/opening-hours \
  -H "Authorization: Bearer $KEY"

Errors: 404 not_found 409 gluedeasy_not_connected 502 gluedeasy_unavailable

DELETE/api/v1/integrations/gluedeasy#

Disconnect GluedEasy

Removes the stored key. Tags in your pages then render their @fallback (or nothing).

Response
{
  "connected": false,
  "via": null,
  "key": null,
  "gluedeasyUrl": "https://gluedeasy.com"
}
curl
curl -s \
  -X DELETE https://lno1g9bz.vibecode.cloud/api/v1/integrations/gluedeasy \
  -H "Authorization: Bearer $KEY"

Projects

GET/api/v1/projects#

List your projects (sites)

Response
{
  "projects": [
    {
      "id": "29ufe",
      "name": "My site",
      "url": "https://29ufe.statichost.dev",
      "homeFile": "index.html",
      "passwordProtected": false,
      "totalBytes": 48213,
      "fileCount": 7,
      "paused": null,
      "domains": []
    }
  ]
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects \
  -H "Authorization: Bearer $KEY"
POST/api/v1/projects#

Create a project

Mints a new site with a random id. Fails with 403 site_limit when the plan allowance is used up.

Parameters

NameInTypeDescription
namebodystringDisplay name (default: the id). Max 80 chars.
descriptionbodystringFree text, max 2000 chars.
Request body
{
  "name": "My site",
  "description": "Landing page"
}
Response
201 {
  "id": "29ufe",
  "name": "My site",
  "url": "https://29ufe.statichost.dev",
  "homeFile": null,
  …
}
curl
curl -s \
  -X POST https://lno1g9bz.vibecode.cloud/api/v1/projects \
  -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' \
  -d '{"name":"My site"}'

Errors: 403 site_limit

GET/api/v1/projects/{id}#

One project, including its file list

Parameters

NameInTypeDescription
id requiredpathstringProject id, e.g. 29ufe
Response
{
  "id": "29ufe",
  …,
  "files": [
    {
      "path": "index.html",
      "size": 1520,
      "contentType": "text/html; charset=utf-8",
      "dynamic": false,
      "updatedAt": "…"
    }
  ]
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe \
  -H "Authorization: Bearer $KEY"
PATCH/api/v1/projects/{id}#

Rename, describe, pick the home file, set or clear the password

Parameters

NameInTypeDescription
namebodystringNew display name.
descriptionbodystringNew description.
homeFilebodystring | nullPath of an uploaded file to serve at /. null = automatic (index.html, home.html).
passwordbodystring | nullProtect the whole site with this password (4–200 chars). null removes protection.
Request body
{
  "name": "Portfolio",
  "homeFile": "start.html",
  "password": "hunter22"
}
Response
{
  "id": "29ufe",
  "name": "Portfolio",
  "homeFile": "start.html",
  "passwordProtected": true,
  …
}
curl
curl -s \
  -X PATCH https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe \
  -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' \
  -d '{"homeFile":"start.html"}'

Errors: 400 invalid_home_file 400 invalid_password

DELETE/api/v1/projects/{id}#

Delete a project and all its files

Response
{
  "deleted": true
}
curl
curl -s \
  -X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe \
  -H "Authorization: Bearer $KEY"
POST/api/v1/projects/{id}/purge#

Purge the CDN cache of a site

Site files are cached at the CDN edge and purged automatically whenever the site changes (uploads, deletes, settings, domains, plan). Use this only if you need a manual refresh. 404 cdn_disabled when the server does not edge-cache sites.

Response
{
  "purged": true,
  "hosts": [
    "29ufe.statichost.dev",
    "www.example.com"
  ]
}
curl
curl -s \
  -X POST https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/purge \
  -H "Authorization: Bearer $KEY"

Errors: 404 cdn_disabled 429 rate_limited 502 cdn_unavailable

Files

PUT/api/v1/projects/{id}/files/{path}#

Upload (or replace) one file

The request body is the raw file bytes (not multipart, not base64). {path} may contain slashes; URL-encode each segment. Send Content-Length when you can β€” oversize uploads are refused before any byte is stored.

Parameters

NameInTypeDescription
path requiredpathstringFile path relative to the site root, e.g. css/site.css. .. is rejected.
Response
201 {
  "path": "css/site.css",
  "size": 5120,
  "contentType": "text/css; charset=utf-8",
  "homeFile": "index.html"
}
curl
curl -s \
  -X PUT \
  --data-binary @css/site.css https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/files/css/site.css \
  -H "Authorization: Bearer $KEY"

Errors: 400 invalid_path 413 site_too_large

GET/api/v1/projects/{id}/files/{path}#

Download one file (raw bytes)

Returns the file exactly as stored. For safety it is always sent as text/plain (text files) or application/octet-stream (everything else) with Content-Disposition: attachment; the real content type is in the X-File-Content-Type header. Use this to read a file before editing it.

Parameters

NameInTypeDescription
path requiredpathstringFile path relative to the site root, e.g. index.html.
Response
200 <the file bytes>   (X-File-Content-Type: text/html; charset=utf-8)
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/files/index.html \
  -H "Authorization: Bearer $KEY" -o index.html

Errors: 404 not_found

DELETE/api/v1/projects/{id}/files/{path}#

Delete one file

Response
{
  "deleted": true
}
curl
curl -s \
  -X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/files/old.html \
  -H "Authorization: Bearer $KEY"
POST/api/v1/projects/{id}/prune#

Delete every file NOT in a keep-list (finish a re-deploy)

Parameters

NameInTypeDescription
keep requiredbodystring[]Paths to keep. Everything else is deleted.
Request body
{
  "keep": [
    "index.html",
    "css/site.css"
  ]
}
Response
{
  "removed": [
    "old.html"
  ]
}
curl
curl -s \
  -X POST https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/prune \
  -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' \
  -d '{"keep":["index.html"]}'
PUT/api/v1/projects/{id}/archive#

Upload a whole site as one .zip

Body = the zip bytes. Every file in the zip is stored at its path inside the zip; a single top-level folder (e.g. dist/) is stripped automatically. Streams β€” the zip is never stored whole.

Parameters

NameInTypeDescription
replacequery0 | 11 = afterwards delete files that were not in the zip.
Response
{
  "uploaded": 12,
  "removed": [
    "old.html"
  ],
  "project": {
    "id": "29ufe",
    …
  }
}
curl
curl -s \
  -X PUT \
  --data-binary @site.zip "https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/archive?replace=1" \
  -H "Authorization: Bearer $KEY"

Errors: 400 invalid_archive 413 site_too_large

Visitor login

GET/api/v1/projects/{id}/access#

Visitor login: state, page tree, rules and roles

available = the server can do visitor login; enabled = on for this site. tree lists every directory and page, rules the rules set (path β†’ rule), roles each role with the paths that use it. authlock.ownerAccess is invited once you were invited to the AuthLock project.

Response
{
  "available": true,
  "enabled": true,
  "tree": [
    {
      "path": "",
      "type": "dir"
    },
    {
      "path": "index.html",
      "type": "page"
    },
    {
      "path": "members/",
      "type": "dir"
    }
  ],
  "rules": {
    "members/": {
      "mode": "roles",
      "roles": [
        "members"
      ]
    }
  },
  "roles": [
    {
      "id": "…",
      "slug": "members",
      "name": "Members",
      "usedOn": [
        "members/"
      ]
    }
  ],
  "authlock": {
    "projectId": "…",
    "dashboardUrl": "https://authlock.eu/dashboard/projects/…",
    "ownerAccess": "invited",
    "origins": [
      "https://29ufe.statichost.dev"
    ]
  }
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access \
  -H "Authorization: Bearer $KEY"

Errors: 404 site_auth_unavailable

POST/api/v1/projects/{id}/access#

Visitor login: turn it on (creates the site's AuthLock project)

First time: creates an AuthLock project for the site (email code + social sign-in, no passwords, optional 2FA), registers the site's origins and invites you as admin. Later: turns it back on with the same project, rules and visitors. Returns the same view as GET.

Response
201 {
  "available": true,
  "enabled": true,
  "tree": [
    …
  ],
  "rules": {},
  "roles": [],
  "authlock": {
    …
  }
}
curl
curl -s \
  -X POST https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access \
  -H "Authorization: Bearer $KEY"

Errors: 404 site_auth_unavailable 429 rate_limited 502 authlock_error

DELETE/api/v1/projects/{id}/access#

Visitor login: turn it off

Every page is open again (the site password still applies). The AuthLock project, rules and roles are kept for when you turn it back on.

Response
{
  "enabled": false
}
curl
curl -s \
  -X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access \
  -H "Authorization: Bearer $KEY"

Errors: 409 site_auth_off

PUT/api/v1/projects/{id}/access/rules#

Visitor login: set who may see which directory or page

Replaces all rules. Keys are paths from the page tree ("" = root, "dir/", "page.html"); values are {"mode":"anonymous"}, {"mode":"signed_in"}, {"mode":"roles","roles":[…]} or null (inherit). Unknown roles are created in AuthLock.

Parameters

NameInTypeDescription
rules requiredbodyobjectpath β†’ rule
Request body
{
  "rules": {
    "members/": {
      "mode": "roles",
      "roles": [
        "members"
      ]
    },
    "account.html": {
      "mode": "signed_in"
    }
  }
}
Response
{
  "enabled": true,
  "rules": {
    …
  },
  "roles": [
    …
  ],
  …
}
curl
curl -s \
  -X PUT https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access/rules \
  -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' \
  -d '{"rules":{"members/":{"mode":"roles","roles":["members"]}}}'

Errors: 400 invalid_path 400 invalid_rule 409 site_auth_off

POST/api/v1/projects/{id}/access/roles#

Visitor login: create a role

Parameters

NameInTypeDescription
slug requiredbodystringLetters, digits, dashes (max 40).
namebodystringDisplay name.
Request body
{
  "slug": "staff",
  "name": "Staff"
}
Response
201 {
  "id": "…",
  "slug": "staff",
  "name": "Staff"
}
curl
curl -s \
  -X POST https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access/roles \
  -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' \
  -d '{"slug":"staff"}'

Errors: 400 invalid_role 409 site_auth_off

GET/api/v1/projects/{id}/access/users#

Visitor login: the site's visitors and their roles

Response
{
  "users": [
    {
      "id": "usr_…",
      "email": "ada@example.com",
      "name": "Ada",
      "status": "active",
      "roles": [
        "members"
      ],
      "lastLoginAt": "…"
    }
  ]
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access/users \
  -H "Authorization: Bearer $KEY"

Errors: 409 site_auth_off

PUT/api/v1/projects/{id}/access/users/{userId}/roles#

Visitor login: set a visitor's roles

The visitor ends up with exactly these roles (unknown ones are created). Takes effect on their next page view within ~5 minutes.

Parameters

NameInTypeDescription
roles requiredbodystring[]Role slugs.
Request body
{
  "roles": [
    "members",
    "staff"
  ]
}
Response
{
  "roles": [
    "members",
    "staff"
  ]
}
curl
curl -s \
  -X PUT https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access/users/usr_123/roles \
  -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' \
  -d '{"roles":["members"]}'

Errors: 404 not_found 409 site_auth_off

AI chat

GET/api/v1/projects/{id}/ai#

AI chat: is it available, and the conversation so far

The editor has an AI chat when the server is configured for it (enabled). messages is your conversation about this site (oldest first).

Response
{
  "enabled": true,
  "messages": [
    {
      "role": "user",
      "text": "Make the header green",
      "at": "…"
    },
    {
      "role": "assistant",
      "text": "Changed the header… <statichost-file path="css/site.css">…</statichost-file>",
      "at": "…"
    }
  ]
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/ai \
  -H "Authorization: Bearer $KEY"
POST/api/v1/projects/{id}/ai/messages#

AI chat: send a message (reply streamed as Server-Sent Events)

Runs one turn of the site's AI assistant. The AI never writes to the site: it answers with complete-file blocks <statichost-file path="…">…</statichost-file> (and <statichost-delete path="…"/>) that the editor applies as unsaved edits β€” to apply them yourself, PUT each file. Send files (path β†’ current content) for files the AI has not seen yet or that changed since its last turn; it remembers earlier ones. The stream carries session, status (workspace starting), text (the reply as it is written), tool, error and a final done event whose reply is the whole answer. Limited per user per day (429 rate_limited).

Parameters

NameInTypeDescription
message requiredbodystringWhat the user wants, 1–20000 chars.
filesbodyobjectpath β†’ current text content (β‰ˆ3 MB max in total).
currentbodystringPath of the file the user has open.
previewPagebodystringPath of the page shown in the preview.
Request body
{
  "message": "Add a contact section",
  "files": {
    "index.html": "<!doctype html>…"
  },
  "current": "index.html"
}
Response
event: textdata: {
  "text":"Added a contact section…"
}event: donedata: {
  "turn":1,
  "status":"completed",
  "reply":"Added a contact section… <statichost-file path="index.html">…</statichost-file>"
}
curl
curl -sN \
  -X POST https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/ai/messages \
  -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' \
  -d '{"message":"Make the header green"}'

Errors: 404 ai_disabled 400 invalid_message 413 context_too_large 429 rate_limited 402 ai_out_of_credits 502 ai_unavailable

DELETE/api/v1/projects/{id}/ai#

AI chat: start over

Closes the AI session (its workspace) and forgets this site's conversation.

Response
{
  "reset": true
}
curl
curl -s \
  -X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/ai \
  -H "Authorization: Bearer $KEY"

Errors: 404 ai_disabled

Custom domains

GET/api/v1/projects/{id}/domains#

List custom domains and their DNS verification state

Response
{
  "domains": [
    {
      "domain": "www.example.com",
      "verified": false
    }
  ],
  "target": "29ufe.statichost.dev"
}
curl
curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/domains \
  -H "Authorization: Bearer $KEY"
POST/api/v1/projects/{id}/domains#

Connect a custom domain (Pro)

Registers the domain and checks DNS immediately. Point CNAME <domain> β†’ <id>.statichost.dev (or TXT _statichost.<domain> = <id> for an apex). Unverified domains are re-checked automatically when traffic arrives.

Parameters

NameInTypeDescription
domain requiredbodystringe.g. www.example.com
Request body
{
  "domain": "www.example.com"
}
Response
201 {
  "domain": "www.example.com",
  "verified": true
}
curl
curl -s \
  -X POST https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/domains \
  -H "Authorization: Bearer $KEY" \
  -H 'content-type: application/json' \
  -d '{"domain":"www.example.com"}'

Errors: 403 plan_required 400 invalid_domain 409 domain_taken

DELETE/api/v1/projects/{id}/domains/{domain}#

Disconnect a custom domain

Response
{
  "deleted": true
}
curl
curl -s \
  -X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/domains/www.example.com \
  -H "Authorization: Bearer $KEY"