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: 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": {
"code": "not_found",
"message": "app not found"
}
}
Endpoints
List all apps owned by the authenticated user.
Auth: Bearer token
curl https://api.xhostd.com/apps \
-H "Authorization: Bearer $XHOST_TOKEN"
{
"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"
}
]
}
]
}
Create a new app. Provisions a git repo and a prod channel.
Auth: Bearer token — requires repo:* scope
Request body
| Field | Type | Description |
|---|---|---|
name | string required | App name. DNS label rules: lowercase, digits, hyphens. Max 40 chars. Must not start with a reserved prefix (git, api, www, admin, preview, staging). |
template | string optional | App template. static (default) or node. |
curl -X POST https://api.xhostd.com/apps \
-H "Authorization: Bearer $XHOST_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-site", "template": "static"}'
{
"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
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Invalid name, reserved prefix, name taken, or invalid template |
| 403 | scope_denied | Token lacks repo:* scope |
Get details of a single app, including all channels.
Auth: Bearer token
curl https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
-H "Authorization: Bearer $XHOST_TOKEN"
{
"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
| Status | Code | When |
|---|---|---|
| 404 | not_found | App does not exist or is not owned by the caller |
Delete an app. Stops all containers, removes the git repo, and cleans up DNS routes.
Auth: Bearer token
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
| Status | Code | When |
|---|---|---|
| 404 | not_found | App does not exist or is not owned by the caller |
List all channels for an app.
Auth: Bearer token
curl https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels \
-H "Authorization: Bearer $XHOST_TOKEN"
[
{
"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"
}
]
Create a new channel (e.g., a preview or staging environment).
Auth: Bearer token — requires channel:* scope
Request body
| Field | Type | Description |
|---|---|---|
name | string required | Channel name. DNS label rules. Cannot be prod (auto-created). |
git_ref_binding | string required | Git ref binding. Format: branch:<name> or branch:* (wildcard). |
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:*"}'
{
"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
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Invalid name, reserved name (prod), or invalid git_ref_binding format |
| 403 | scope_denied | Token lacks channel:* scope |
| 404 | not_found | App not found |
Get details of a single channel.
Auth: Bearer token
curl https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/7c9e6679-7425-40de-944b-e07fc1f90ae7 \
-H "Authorization: Bearer $XHOST_TOKEN"
{
"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
| Status | Code | When |
|---|---|---|
| 404 | not_found | App or channel not found |
Delete a channel. Stops the container and removes DNS routes. Cannot delete the prod channel.
Auth: Bearer token
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
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Attempted to delete the prod channel |
| 404 | not_found | App or channel not found |
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
| Field | Type | Description |
|---|---|---|
sha | string required | A 40-character hex SHA or a branch name (e.g. master, HEAD). The server resolves branch names to SHAs at deploy time. |
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"}'
{
"deploy_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"channel_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"status": "queued"
}
Errors
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Invalid SHA format |
| 403 | scope_denied | Token lacks deploy:* scope |
| 404 | not_found | App or channel not found |
Retrieve the build/deploy log for a specific deploy.
Auth: Bearer token
Query parameters
| Param | Type | Description |
|---|---|---|
deploy | UUID required | The deploy ID returned by the deploy endpoint |
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"
[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
| Status | Code | When |
|---|---|---|
| 404 | not_found | Deploy not found, or log not yet available |
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
| Field | Type | Description |
|---|---|---|
key | string required | Env 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. |
value | string required | The value. Encrypted at rest. |
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
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Invalid key format or reserved key |
| 403 | scope_denied | Token lacks deploy:* scope |
| 404 | not_found | App not found |
Delete an environment variable. Takes effect on the next deploy.
Auth: Bearer token
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
| Status | Code | When |
|---|---|---|
| 404 | not_found | App 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.
Inspect the channel's Postgres schema: name, role, status, live connection count, storage usage.
Auth: Bearer token
curl https://api.xhostd.com/apps/f47ac10b-58cc-4372-a567-0e02b2c3d479/channels/7c9e6679-7425-40de-944b-e07fc1f90ae7/postgres \
-H "Authorization: Bearer $XHOST_TOKEN"
{
"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
| Field | Type | Description |
|---|---|---|
db_name | string | The user-scoped Postgres database name |
schema_name | string | The channel's schema inside that database |
role_name | string | The Postgres role used in DATABASE_URL |
status | string | One of provisioning, ready, failed |
last_error | string or null | Provisioner error message if status is failed |
connection_count | integer | Live connections currently held by this role |
connection_limit | integer | Configured CONNECTION LIMIT for the role |
password_set | boolean | Whether the role has a stored password |
storage_bytes | integer | Total size of the channel's schema on disk |
Errors
| Status | Code | When |
|---|---|---|
| 404 | not_found | App, channel, or schema row not found |
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
| Field | Type | Description |
|---|---|---|
confirm_schema_name | string required | Must match the channel's current schema_name exactly. Acts as a typed confirmation. |
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
| Status | Code | When |
|---|---|---|
| 400 | invalid_confirmation | confirm_schema_name does not match the channel's schema |
| 404 | not_found | App, channel, or schema row not found |
| 409 | conflict | Channel postgres is not in ready state |
| 503 | postgres_unavailable | Postgres admin pool is not configured (degraded mode) |
Stream a pg_dump of the channel's schema. Single schema only. Useful for backups and migrating data between channels.
Auth: Bearer token
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
| Status | Code | When |
|---|---|---|
| 404 | not_found | App, channel, or schema row not found |
| 409 | conflict | Channel postgres is not in ready state |
| 503 | postgres_unavailable | Postgres admin pool is not configured (degraded mode) |
Report total Postgres storage and schema count for the authenticated user, across all channels.
Auth: Bearer token
curl https://api.xhostd.com/me/postgres/storage \
-H "Authorization: Bearer $XHOST_TOKEN"
{
"database_size_bytes": 1572864,
"schema_count": 5
}
Response fields
| Field | Type | Description |
|---|---|---|
database_size_bytes | integer | Total size of the user's u_<user_id> database |
schema_count | integer | Number of ch_* schemas in that database |
Errors
| Status | Code | When |
|---|---|---|
| 503 | postgres_unavailable | Postgres admin pool is not configured (degraded mode) |
Create a new API token for the authenticated user.
Auth: Bearer token
Request body
| Field | Type | Description |
|---|---|---|
label | string optional | Human-readable label (e.g. "ci", "laptop") |
curl -X POST https://api.xhostd.com/tokens \
-H "Authorization: Bearer $XHOST_TOKEN" \
-H "Content-Type: application/json" \
-d '{"label": "ci"}'
{
"token_id": "c56a4180-65aa-42ec-a945-5fd21dec0538",
"plaintext": "xh_live_newtoken123...",
"scopes": ["repo:*", "deploy:*", "channel:*"],
"label": "ci",
"created_at": "2026-04-22T11:00:00Z"
}
token_id.
Revoke a token. The token is immediately invalidated.
Auth: Bearer token
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
| Status | Code | When |
|---|---|---|
| 404 | not_found | Token not found or not owned by the caller |
Get dashboard statistics for the authenticated user.
Auth: Bearer token
curl https://api.xhostd.com/api/user/stats \
-H "Authorization: Bearer $XHOST_TOKEN"
{
"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
| Field | Type | Description |
|---|---|---|
platform | object | App, channel, and deploy counts |
resources | object | Memory and CPU usage from cgroup budgets (zero if not configured) |
sites | array | List of deployed channels with status and latest deploy info |
collected_at | string | Timestamp when stats were collected |
Reference
Hostname derivation
Every channel gets a unique hostname derived from the app name, channel name, and username.
| Channel | Hostname pattern | Example |
|---|---|---|
prod | <app>-<user>.xhostd.com | my-site-alice.xhostd.com |
| Any other | <channel>-<app>-<user>.xhostd.com | staging-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
| Status | Meaning |
|---|---|
provisioning | Channel created, no container running yet. Waiting for first deploy. |
running | Container is live and serving traffic. |
Deploy status values
| Status | Meaning |
|---|---|
queued | Deploy accepted and waiting to be processed. |
running | Deploy is actively building/starting the container. |
success | Deploy completed and the site is live. |
failed | Deploy 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.
| Format | Meaning | Example |
|---|---|---|
branch:<name> | Bind to a specific branch | branch:master |
branch:* | Accept any branch (wildcard) | branch:* |
Error codes
| Code | HTTP Status | Meaning |
|---|---|---|
auth_required | 401 | No token provided or token is invalid |
token_invalid | 401 | Token does not exist or invite is invalid |
token_revoked | 401 | Token has been revoked |
scope_denied | 403 | Token lacks the required scope |
scope_reserved | 403 | Requested scope is reserved and cannot be used |
permission_denied | 403 | Caller is not authorized for this action (e.g. not admin) |
admin_not_configured | 403 | Admin user has not been bootstrapped on this instance |
not_found | 404 | Resource does not exist or is not owned by the caller |
bad_request | 400 | Invalid input (name format, reserved name, etc.) |
bad_gateway | 502 | Upstream dependency failed |
internal_error | 500 | Unexpected server error |
Token scopes
| Scope | Grants access to | Default |
|---|---|---|
repo:* | Create and manage apps (git repo provisioning) | Yes |
deploy:* | Trigger deploys and manage env vars | Yes |
channel:* | Create and delete channels | Yes |
db:* | Reserved for future use. Cannot be requested. | No |
All tokens receive the three default scopes. Custom scope selection is not yet supported.