xhost
Sign in

API Reference

Base URL: https://api.xhostd.com

Authentication

All endpoints require a bearer token in the Authorization header. Tokens are prefixed with xh_. Mint one at https://xhostd.com/tokens (the plaintext is shown once). If a call returns 401, the token is dead — re-mint at the same URL.

Authorization header
Authorization: Bearer xh_live_abc123...

Missing or invalid tokens return a 401 response. Insufficient scopes return 403.

Error envelope

Every error response uses the same shape:

Error response
{
  "error": {
    "code": "not_found",
    "message": "app not found"
  }
}

Endpoints

Signing up happens in the browser via Google sign-in at xhostd.com. There is no signup API. After signing in, create API tokens for CLI/agent use on the dashboard.
GET /apps

List all apps owned by the authenticated user.

Auth: Bearer token

Request
curl https://api.xhostd.com/apps \
  -H "Authorization: Bearer $XHOST_TOKEN"
Response — 200
{
  "apps": [
    {
      "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
      "name": "my-site",
      "repo_url": "https://git.xhostd.com/alice/my-site.git",
      "template": "static",
      "created_at": "2026-04-22T10:30:00Z",
      "channels": [
        {
          "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
          "name": "prod",
          "hostname": "my-site-alice.xhostd.com",
          "git_ref_binding": "branch:master",
          "current_sha": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
          "status": "running"
        }
      ]
    }
  ]
}
POST /apps

Create a new app. Provisions a git repo and a prod channel.

Auth: Bearer token — requires repo:* scope

Request body

FieldTypeDescription
namestring requiredApp name. DNS label rules: lowercase, digits, hyphens. Max 40 chars. Must not start with a reserved prefix (git, api, www, admin, preview, staging).
templatestring optionalApp template. static (default) or node.
Request
curl -X POST https://api.xhostd.com/apps \
  -H "Authorization: Bearer $XHOST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-site", "template": "static"}'
Response — 200
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "name": "my-site",
  "repo_url": "https://git.xhostd.com/alice/my-site.git",
  "template": "static",
  "created_at": "2026-04-22T10:30:00Z",
  "channels": [
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "name": "prod",
      "hostname": "my-site-alice.xhostd.com",
      "git_ref_binding": "branch:master",
      "current_sha": null,
      "status": "provisioning"
    }
  ]
}

Errors

StatusCodeWhen
400bad_requestInvalid name, reserved prefix, name taken, or invalid template
403scope_deniedToken lacks repo:* scope
GET /apps/{app_id}

Get details of a single app, including all channels.

Auth: Bearer token

Request
curl https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
  -H "Authorization: Bearer $XHOST_TOKEN"
Response — 200
{
  "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "name": "my-site",
  "repo_url": "https://git.xhostd.com/alice/my-site.git",
  "template": "static",
  "created_at": "2026-04-22T10:30:00Z",
  "channels": [
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "name": "prod",
      "hostname": "my-site-alice.xhostd.com",
      "git_ref_binding": "branch:master",
      "current_sha": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
      "status": "running"
    }
  ]
}

Errors

StatusCodeWhen
404not_foundApp does not exist or is not owned by the caller
DELETE /apps/{app_id}

Delete an app. Stops all containers, removes the git repo, and cleans up DNS routes.

Auth: Bearer token

Request
curl -X DELETE https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
  -H "Authorization: Bearer $XHOST_TOKEN"

Returns 204 No Content on success.

Errors

StatusCodeWhen
404not_foundApp does not exist or is not owned by the caller
GET /apps/{app_id}/channels

List all channels for an app.

Auth: Bearer token

Request
curl https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels \
  -H "Authorization: Bearer $XHOST_TOKEN"
Response — 200
[
  {
    "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "name": "prod",
    "hostname": "my-site-alice.xhostd.com",
    "git_ref_binding": "branch:master",
    "current_sha": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
    "status": "running"
  },
  {
    "id": "a3bb189e-8bf9-3888-9912-ace4e6543002",
    "name": "staging",
    "hostname": "staging-my-site-alice.xhostd.com",
    "git_ref_binding": "branch:*",
    "current_sha": null,
    "status": "provisioning"
  }
]
POST /apps/{app_id}/channels

Create a new channel (e.g., a preview or staging environment).

Auth: Bearer token — requires channel:* scope

Request body

FieldTypeDescription
namestring requiredChannel name. DNS label rules. Cannot be prod (auto-created).
git_ref_bindingstring requiredGit ref binding. Format: branch:<name> or branch:* (wildcard).
Request
curl -X POST https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels \
  -H "Authorization: Bearer $XHOST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "staging", "git_ref_binding": "branch:*"}'
Response — 200
{
  "id": "a3bb189e-8bf9-3888-9912-ace4e6543002",
  "name": "staging",
  "hostname": "staging-my-site-alice.xhostd.com",
  "git_ref_binding": "branch:*",
  "current_sha": null,
  "status": "provisioning"
}

Errors

StatusCodeWhen
400bad_requestInvalid name, reserved name (prod), or invalid git_ref_binding format
403scope_deniedToken lacks channel:* scope
404not_foundApp not found
GET /apps/{app_id}/channels/{channel_id}

Get details of a single channel.

Auth: Bearer token

Request
curl https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
  -H "Authorization: Bearer $XHOST_TOKEN"
Response — 200
{
  "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "name": "prod",
  "hostname": "my-site-alice.xhostd.com",
  "git_ref_binding": "branch:master",
  "current_sha": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
  "status": "running"
}

Errors

StatusCodeWhen
404not_foundApp or channel not found
DELETE /apps/{app_id}/channels/{channel_id}

Delete a channel. Stops the container and removes DNS routes. Cannot delete the prod channel.

Auth: Bearer token

Request
curl -X DELETE https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/a3bb189e-8bf9-3888-9912-ace4e6543002 \
  -H "Authorization: Bearer $XHOST_TOKEN"

Returns 204 No Content on success.

Errors

StatusCodeWhen
400bad_requestAttempted to delete the prod channel
404not_foundApp or channel not found
POST /apps/{app_id}/channels/{channel_id}/deploy

Trigger a deploy. Pulls the specified SHA or branch from git, builds the container, and brings it live.

Auth: Bearer token — requires deploy:* scope

Request body

FieldTypeDescription
shastring requiredA 40-character hex SHA or a branch name (e.g. master, HEAD). The server resolves branch names to SHAs at deploy time.
Request
curl -X POST https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/7c9e6679-7425-40de-944b-e07fc1f90ae7/deploy \
  -H "Authorization: Bearer $XHOST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sha": "HEAD"}'
Response — 200
{
  "deploy_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
  "channel_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "status": "queued"
}

Errors

StatusCodeWhen
400bad_requestInvalid SHA format
403scope_deniedToken lacks deploy:* scope
404not_foundApp or channel not found
GET /apps/{app_id}/channels/{channel_id}/logs?deploy={deploy_id}

Retrieve the build/deploy log for a specific deploy.

Auth: Bearer token

Query parameters

ParamTypeDescription
deployUUID requiredThe deploy ID returned by the deploy endpoint
Request
curl "https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/7c9e6679-7425-40de-944b-e07fc1f90ae7/logs?deploy=9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" \
  -H "Authorization: Bearer $XHOST_TOKEN"
Response — 200 (text/plain)
[2026-04-22T10:31:00Z] git-sync: resolved HEAD -> a1b2c3d4
[2026-04-22T10:31:01Z] starting container...
[2026-04-22T10:31:03Z] health check passed
[2026-04-22T10:31:03Z] deploy complete

Errors

StatusCodeWhen
404not_foundDeploy not found, or log not yet available
POST /apps/{app_id}/env

Set an environment variable for the app. Creates or updates the key. Takes effect on the next deploy.

Auth: Bearer token — requires deploy:* scope

Request body

FieldTypeDescription
keystring requiredEnv var name. Uppercase letters, digits, underscores. Must match ^[A-Z_][A-Z0-9_]*$. Reserved (rejected): XHOST_USER, XHOST_SHA, DATABASE_URL, DATABASE_HOST, DATABASE_PASSWORD — the DATABASE_* trio is auto-injected per channel.
valuestring requiredThe value. Encrypted at rest.
Request
curl -X POST https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/env \
  -H "Authorization: Bearer $XHOST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key": "STRIPE_SECRET_KEY", "value": "sk_live_..."}'

Returns 204 No Content on success.

Errors

StatusCodeWhen
400bad_requestInvalid key format or reserved key
403scope_deniedToken lacks deploy:* scope
404not_foundApp not found
DELETE /apps/{app_id}/env/{key}

Delete an environment variable. Takes effect on the next deploy.

Auth: Bearer token

Request
curl -X DELETE https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/env/STRIPE_SECRET_KEY \
  -H "Authorization: Bearer $XHOST_TOKEN"

Returns 204 No Content on success.

Errors

StatusCodeWhen
404not_foundApp not found

Postgres

Every channel automatically gets its own Postgres schema inside the user's database, with a dedicated role and a DATABASE_URL injected into the container at start. Schema migrations are user-managed — put alembic upgrade head, prisma migrate deploy, or equivalent in your install.sh. The database is reachable before install.sh runs.

GET /apps/{app_id}/channels/{channel_id}/postgres

Inspect the channel's Postgres schema: name, role, status, live connection count, storage usage.

Auth: Bearer token

Request
curl https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/7c9e6679-7425-40de-944b-e07fc1f90ae7/postgres \
  -H "Authorization: Bearer $XHOST_TOKEN"
Response — 200
{
  "db_name": "u_550e8400e29b41d4a716446655440000",
  "schema_name": "ch_7c9e66797425",
  "role_name": "r_7c9e66797425",
  "status": "ready",
  "last_error": null,
  "connection_count": 2,
  "connection_limit": 20,
  "password_set": true,
  "storage_bytes": 81920
}

Response fields

FieldTypeDescription
db_namestringThe user-scoped Postgres database name
schema_namestringThe channel's schema inside that database
role_namestringThe Postgres role used in DATABASE_URL
statusstringOne of provisioning, ready, failed
last_errorstring or nullProvisioner error message if status is failed
connection_countintegerLive connections currently held by this role
connection_limitintegerConfigured CONNECTION LIMIT for the role
password_setbooleanWhether the role has a stored password
storage_bytesintegerTotal size of the channel's schema on disk

Errors

StatusCodeWhen
404not_foundApp, channel, or schema row not found
POST /apps/{app_id}/channels/{channel_id}/postgres/reset

Drop and recreate the channel's schema. The role and password are preserved, so the same DATABASE_URL keeps working. All data and migration history in the schema is destroyed.

Auth: Bearer token

Request body

FieldTypeDescription
confirm_schema_namestring requiredMust match the channel's current schema_name exactly. Acts as a typed confirmation.
Request
curl -X POST https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/7c9e6679-7425-40de-944b-e07fc1f90ae7/postgres/reset \
  -H "Authorization: Bearer $XHOST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirm_schema_name": "ch_7c9e66797425"}'

Returns 204 No Content on success.

Errors

StatusCodeWhen
400invalid_confirmationconfirm_schema_name does not match the channel's schema
404not_foundApp, channel, or schema row not found
409conflictChannel postgres is not in ready state
503postgres_unavailablePostgres admin pool is not configured (degraded mode)
GET /apps/{app_id}/channels/{channel_id}/postgres/dump

Stream a pg_dump of the channel's schema. Single schema only. Useful for backups and migrating data between channels.

Auth: Bearer token

Request
curl https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/7c9e6679-7425-40de-944b-e07fc1f90ae7/postgres/dump \
  -H "Authorization: Bearer $XHOST_TOKEN" \
  -o channel.sql

Returns 200 OK with Content-Type: application/sql and a Content-Disposition attachment header. The body is the raw pg_dump output, streamed.

Errors

StatusCodeWhen
404not_foundApp, channel, or schema row not found
409conflictChannel postgres is not in ready state
503postgres_unavailablePostgres admin pool is not configured (degraded mode)
GET /me/postgres/storage

Report total Postgres storage and schema count for the authenticated user, across all channels.

Auth: Bearer token

Request
curl https://api.xhostd.com/me/postgres/storage \
  -H "Authorization: Bearer $XHOST_TOKEN"
Response — 200
{
  "database_size_bytes": 1572864,
  "schema_count": 5
}

Response fields

FieldTypeDescription
database_size_bytesintegerTotal size of the user's u_<user_id> database
schema_countintegerNumber of ch_* schemas in that database

Errors

StatusCodeWhen
503postgres_unavailablePostgres admin pool is not configured (degraded mode)
POST /tokens

Create a new API token for the authenticated user.

Auth: Bearer token

Request body

FieldTypeDescription
labelstring optionalHuman-readable label (e.g. "ci", "laptop")
Request
curl -X POST https://api.xhostd.com/tokens \
  -H "Authorization: Bearer $XHOST_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"label": "ci"}'
Response — 200
{
  "token_id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
  "plaintext": "xh_live_newtoken123...",
  "scopes": ["repo:*", "deploy:*", "channel:*"],
  "label": "ci",
  "created_at": "2026-04-22T11:00:00Z"
}
Save the plaintext token. It is only returned once at creation time. Subsequent API calls reference the token by its token_id.
DELETE /tokens/{token_id}

Revoke a token. The token is immediately invalidated.

Auth: Bearer token

Request
curl -X DELETE https://api.xhostd.com/tokens/c56a4180-65aa-42ec-a945-5fd21dec0538 \
  -H "Authorization: Bearer $XHOST_TOKEN"

Returns 204 No Content on success.

Errors

StatusCodeWhen
404not_foundToken not found or not owned by the caller
GET /api/user/stats

Get dashboard statistics for the authenticated user.

Auth: Bearer token

Request
curl https://api.xhostd.com/api/user/stats \
  -H "Authorization: Bearer $XHOST_TOKEN"
Response — 200
{
  "username": "alice",
  "user_id": "550e8400-e29b-41d4-a716-446655440000",
  "platform": {
    "apps": 3,
    "channels": 5,
    "running_channels": 4,
    "deploys_last_hour": 1,
    "deploys_last_day": 7,
    "success_last_day": 6,
    "failed_last_day": 1
  },
  "resources": {
    "mem_current_mb": 45.2,
    "mem_limit_mb": 256.0,
    "mem_percent": 17.7,
    "cpu_current_percent": 2.5,
    "cpu_avg_percent": 1.1,
    "cpu_usage_sec": 142.5
  },
  "sites": [
    {
      "hostname": "my-site-alice.xhostd.com",
      "repo": "alice/my-site",
      "branch": "master",
      "status": "running",
      "sha": "abc1234",
      "latest_deploy_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
      "latest_deploy_status": "success"
    }
  ],
  "collected_at": "2026-04-24 10:30:00 UTC"
}

Response fields

FieldTypeDescription
platformobjectApp, channel, and deploy counts
resourcesobjectMemory and CPU usage from cgroup budgets (zero if not configured)
sitesarrayList of deployed channels with status and latest deploy info
collected_atstringTimestamp when stats were collected

Reference

Hostname derivation

Every channel gets a unique hostname derived from the app name, channel name, and username.

ChannelHostname patternExample
prod<app>-<user>.xhostd.commy-site-alice.xhostd.com
Any other<channel>-<app>-<user>.xhostd.comstaging-my-site-alice.xhostd.com

All name components must be valid DNS labels: lowercase letters, digits, and hyphens, with no leading or trailing hyphen and a maximum length of 40 characters.

Reserved prefixes

The following names cannot be used as app names (and cannot start app names followed by a hyphen):

git, api, www, admin, preview, staging

Channel status values

StatusMeaning
provisioningChannel created, no container running yet. Waiting for first deploy.
runningContainer is live and serving traffic.

Deploy status values

StatusMeaning
queuedDeploy accepted and waiting to be processed.
runningDeploy is actively building/starting the container.
successDeploy completed and the site is live.
failedDeploy failed. Check the deploy logs for details.

git_ref_binding format

The git_ref_binding field controls which git refs a channel accepts for deployment.

FormatMeaningExample
branch:<name>Bind to a specific branchbranch:master
branch:*Accept any branch (wildcard)branch:*

Error codes

CodeHTTP StatusMeaning
auth_required401No token provided or token is invalid
token_invalid401Token does not exist or invite is invalid
token_revoked401Token has been revoked
scope_denied403Token lacks the required scope
scope_reserved403Requested scope is reserved and cannot be used
permission_denied403Caller is not authorized for this action (e.g. not admin)
admin_not_configured403Admin user has not been bootstrapped on this instance
not_found404Resource does not exist or is not owned by the caller
bad_request400Invalid input (name format, reserved name, etc.)
bad_gateway502Upstream dependency failed
internal_error500Unexpected server error

Token scopes

ScopeGrants access toDefault
repo:*Create and manage apps (git repo provisioning)Yes
deploy:*Trigger deploys and manage env varsYes
channel:*Create and delete channelsYes
db:*Reserved for future use. Cannot be requested.No

All tokens receive the three default scopes. Custom scope selection is not yet supported.