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
| Plan | Sites | Max size per site | Custom domains | Badge |
|---|---|---|---|---|
| Free ($0) | 2 | 100 MB | no | "Hosted on statichost.dev" footer |
| Pro ($19.99/mo, or $203.90/yr) | 10 | 1000 MB | yes | none |
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
POST /api/v1/projectswith{"name": "My site"}β note the returnedidandurl. (To update an existing site instead, find it withGET /api/v1/projects.)- 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. - Optional, when re-deploying:
POST /api/v1/projects/{id}/prunewith{"keep": [every path you just uploaded]}to delete files that no longer exist locally. - The site is live at
url.index.html(orhome.html) in the root becomes the home page automatically; otherwise sethomeFilewithPATCH /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";"/")')"
doneHow sites are served
/serves the home file./abouttriesabout, thenabout.html, thenabout/index.html. A404.htmlin 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 (
PATCHwith{"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 createCNAME <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>
- 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/gluedeasywith 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. - 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=valuepairs 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 barenamealone istrue.- 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 rolesnullβ 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
/api/v1/me#Who am I β the key owner, plan, limits and usage
{
"user": {
"id": "k2j4β¦",
"email": "you@example.com",
"name": "You"
},
"plan": "free",
"limits": {
"sites": 2,
"maxSiteBytes": 104857600,
"customDomains": false,
"badge": true
},
"usage": {
"sites": 1
}
}curl -s https://lno1g9bz.vibecode.cloud/api/v1/me \
-H "Authorization: Bearer $KEY"/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.
{
"connected": true,
"via": "key",
"key": "sk_4f2β¦9a1c Β· you@example.com",
"gluedeasyUrl": "https://gluedeasy.com"
}curl -s https://lno1g9bz.vibecode.cloud/api/v1/integrations/gluedeasy \
-H "Authorization: Bearer $KEY"/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
| Name | In | Type | Description |
|---|---|---|---|
apiKey required | body | string | A GluedEasy sk_β¦ key. |
{
"apiKey": "sk_..."
}{
"connected": true,
"via": "key",
"key": "sk_4f2β¦9a1c Β· you@example.com",
"gluedeasyUrl": "https://gluedeasy.com"
}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
/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.
{
"workflows": [
{
"id": "wf_abc123",
"slug": "opening-hours",
"name": "Opening hours",
"description": "Today's hours for the shop"
}
]
}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
/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.
{
"id": "wf_abc123",
"slug": "opening-hours",
"name": "Opening hours",
"description": "β¦",
"entryStep": "hours",
"inputs": [
"day",
"shop"
],
"steps": [
{
"id": "hours",
"trigger": "rest",
"inputs": [
"day",
"shop"
]
}
]
}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
/api/v1/integrations/gluedeasy#Disconnect GluedEasy
Removes the stored key. Tags in your pages then render their @fallback (or nothing).
{
"connected": false,
"via": null,
"key": null,
"gluedeasyUrl": "https://gluedeasy.com"
}curl -s \
-X DELETE https://lno1g9bz.vibecode.cloud/api/v1/integrations/gluedeasy \
-H "Authorization: Bearer $KEY"Projects
/api/v1/projects#List your projects (sites)
{
"projects": [
{
"id": "29ufe",
"name": "My site",
"url": "https://29ufe.statichost.dev",
"homeFile": "index.html",
"passwordProtected": false,
"totalBytes": 48213,
"fileCount": 7,
"paused": null,
"domains": []
}
]
}curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects \
-H "Authorization: Bearer $KEY"/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
| Name | In | Type | Description |
|---|---|---|---|
name | body | string | Display name (default: the id). Max 80 chars. |
description | body | string | Free text, max 2000 chars. |
{
"name": "My site",
"description": "Landing page"
}201 {
"id": "29ufe",
"name": "My site",
"url": "https://29ufe.statichost.dev",
"homeFile": null,
β¦
}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
/api/v1/projects/{id}#One project, including its file list
Parameters
| Name | In | Type | Description |
|---|---|---|---|
id required | path | string | Project id, e.g. 29ufe |
{
"id": "29ufe",
β¦,
"files": [
{
"path": "index.html",
"size": 1520,
"contentType": "text/html; charset=utf-8",
"dynamic": false,
"updatedAt": "β¦"
}
]
}curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe \
-H "Authorization: Bearer $KEY"/api/v1/projects/{id}#Rename, describe, pick the home file, set or clear the password
Parameters
| Name | In | Type | Description |
|---|---|---|---|
name | body | string | New display name. |
description | body | string | New description. |
homeFile | body | string | null | Path of an uploaded file to serve at /. null = automatic (index.html, home.html). |
password | body | string | null | Protect the whole site with this password (4β200 chars). null removes protection. |
{
"name": "Portfolio",
"homeFile": "start.html",
"password": "hunter22"
}{
"id": "29ufe",
"name": "Portfolio",
"homeFile": "start.html",
"passwordProtected": true,
β¦
}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
/api/v1/projects/{id}#Delete a project and all its files
{
"deleted": true
}curl -s \
-X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe \
-H "Authorization: Bearer $KEY"/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.
{
"purged": true,
"hosts": [
"29ufe.statichost.dev",
"www.example.com"
]
}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
/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
| Name | In | Type | Description |
|---|---|---|---|
path required | path | string | File path relative to the site root, e.g. css/site.css. .. is rejected. |
201 {
"path": "css/site.css",
"size": 5120,
"contentType": "text/css; charset=utf-8",
"homeFile": "index.html"
}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
/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
| Name | In | Type | Description |
|---|---|---|---|
path required | path | string | File path relative to the site root, e.g. index.html. |
200 <the file bytes> (X-File-Content-Type: text/html; charset=utf-8)curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/files/index.html \
-H "Authorization: Bearer $KEY" -o index.htmlErrors: 404 not_found
/api/v1/projects/{id}/files/{path}#Delete one file
{
"deleted": true
}curl -s \
-X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/files/old.html \
-H "Authorization: Bearer $KEY"/api/v1/projects/{id}/prune#Delete every file NOT in a keep-list (finish a re-deploy)
Parameters
| Name | In | Type | Description |
|---|---|---|---|
keep required | body | string[] | Paths to keep. Everything else is deleted. |
{
"keep": [
"index.html",
"css/site.css"
]
}{
"removed": [
"old.html"
]
}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"]}'/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
| Name | In | Type | Description |
|---|---|---|---|
replace | query | 0 | 1 | 1 = afterwards delete files that were not in the zip. |
{
"uploaded": 12,
"removed": [
"old.html"
],
"project": {
"id": "29ufe",
β¦
}
}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
/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.
{
"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 -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access \
-H "Authorization: Bearer $KEY"Errors: 404 site_auth_unavailable
/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.
201 {
"available": true,
"enabled": true,
"tree": [
β¦
],
"rules": {},
"roles": [],
"authlock": {
β¦
}
}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
/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.
{
"enabled": false
}curl -s \
-X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access \
-H "Authorization: Bearer $KEY"Errors: 409 site_auth_off
/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
| Name | In | Type | Description |
|---|---|---|---|
rules required | body | object | path β rule |
{
"rules": {
"members/": {
"mode": "roles",
"roles": [
"members"
]
},
"account.html": {
"mode": "signed_in"
}
}
}{
"enabled": true,
"rules": {
β¦
},
"roles": [
β¦
],
β¦
}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
/api/v1/projects/{id}/access/roles#Visitor login: create a role
Parameters
| Name | In | Type | Description |
|---|---|---|---|
slug required | body | string | Letters, digits, dashes (max 40). |
name | body | string | Display name. |
{
"slug": "staff",
"name": "Staff"
}201 {
"id": "β¦",
"slug": "staff",
"name": "Staff"
}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
/api/v1/projects/{id}/access/users#Visitor login: the site's visitors and their roles
{
"users": [
{
"id": "usr_β¦",
"email": "ada@example.com",
"name": "Ada",
"status": "active",
"roles": [
"members"
],
"lastLoginAt": "β¦"
}
]
}curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/access/users \
-H "Authorization: Bearer $KEY"Errors: 409 site_auth_off
/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
| Name | In | Type | Description |
|---|---|---|---|
roles required | body | string[] | Role slugs. |
{
"roles": [
"members",
"staff"
]
}{
"roles": [
"members",
"staff"
]
}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
/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).
{
"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 -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/ai \
-H "Authorization: Bearer $KEY"/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
| Name | In | Type | Description |
|---|---|---|---|
message required | body | string | What the user wants, 1β20000 chars. |
files | body | object | path β current text content (β3 MB max in total). |
current | body | string | Path of the file the user has open. |
previewPage | body | string | Path of the page shown in the preview. |
{
"message": "Add a contact section",
"files": {
"index.html": "<!doctype html>β¦"
},
"current": "index.html"
}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 -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
/api/v1/projects/{id}/ai#AI chat: start over
Closes the AI session (its workspace) and forgets this site's conversation.
{
"reset": true
}curl -s \
-X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/ai \
-H "Authorization: Bearer $KEY"Errors: 404 ai_disabled
Custom domains
/api/v1/projects/{id}/domains#List custom domains and their DNS verification state
{
"domains": [
{
"domain": "www.example.com",
"verified": false
}
],
"target": "29ufe.statichost.dev"
}curl -s https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/domains \
-H "Authorization: Bearer $KEY"/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
| Name | In | Type | Description |
|---|---|---|---|
domain required | body | string | e.g. www.example.com |
{
"domain": "www.example.com"
}201 {
"domain": "www.example.com",
"verified": true
}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
/api/v1/projects/{id}/domains/{domain}#Disconnect a custom domain
{
"deleted": true
}curl -s \
-X DELETE https://lno1g9bz.vibecode.cloud/api/v1/projects/29ufe/domains/www.example.com \
-H "Authorization: Bearer $KEY"