REST API Reference
All endpoints are under /v1/. Authenticate with X-Platform-API-Key header.
Authentication
Every request must include your platform API key:
X-Platform-API-Key: th_live_your_key_here
Error Format
All errors return a structured JSON body:
{
"error": {
"code": "domain_taken",
"message": "The domain mysite.com is already provisioned",
"next_action": "Choose a different domain or check if you own this domain",
"retryable": false
}
}
Rate Limits
Rate limits are per-platform and vary by plan tier. Headers included on every response:
| Header | Description |
|---|---|
X-Rate-Limit-Limit | Max requests per day |
X-Rate-Limit-Remaining | Requests remaining today |
X-Rate-Limit-Reset | Seconds until limit resets |
POST /v1/domains
Provision a new custom domain.
Request
| Field | Type | Required | Description |
|---|---|---|---|
domain | string | yes | Custom domain to provision (e.g. "mysite.com") |
origin_url | string | yes | Origin URL to proxy to |
end_user_email | string | yes | End user's email for billing |
idempotency_key | string | no | Prevent duplicate provisions on retry |
Response (201)
{
"id": "uuid",
"domain": "mysite.com",
"status": "pending_payment",
"dns_instructions": {
"type": "CNAME",
"name": "mysite.com",
"value": "cname.thin.host",
"human_readable": "Set a CNAME record for mysite.com pointing to cname.thin.host"
},
"claim_url": "https://thin.host/claim/abc123...",
"next_action": "Direct the user to the claim_url to complete payment and DNS setup",
"expected_completion_seconds": 300
}
GET /v1/domains/:id
Get full domain status.
Response (200)
{
"id": "uuid",
"hostname": "mysite.com",
"origin_url": "https://myapp.vercel.app",
"status": "active",
"dns_status": "resolved",
"ssl_status": "active",
"payment_status": "paid",
"ready": true,
"created_at": "2026-04-25T00:00:00Z",
"updated_at": "2026-04-25T01:00:00Z"
}
PATCH /v1/domains/:id
Update the origin URL for an existing domain.
Request
{ "origin_url": "https://new-deploy.vercel.app" }
Response (200)
{
"id": "uuid",
"domain": "mysite.com",
"origin_url": "https://new-deploy.vercel.app",
"status": "active",
"updated_at": "2026-04-25T02:00:00Z"
}
DELETE /v1/domains/:id
Release a domain. Removes proxy config and marks as released. Not reversible.
Response (200)
{
"id": "uuid",
"domain": "mysite.com",
"status": "released",
"released_at": "2026-04-25T03:00:00Z"
}
GET /v1/domains
List domains with pagination and filtering.
Query Parameters
| Param | Default | Description |
|---|---|---|
status | all | Filter by status enum |
page | 1 | Page number |
limit | 50 | Results per page (max 100) |
Response (200)
{
"domains": [ ... ],
"total": 42,
"page": 1,
"per_page": 50,
"has_more": false
}
POST /v1/websites
Publish a new hosted website from JSON — the endpoint behind the MCP publish_website tool. Requires an API key created from the dashboard (linked to your account); unlinked platform keys get account_not_linked. The site is live at the returned url immediately.
Request
| Field | Type | Required | Description |
|---|---|---|---|
title | string | yes | Site title |
slug | string | no | URL slug (site lives at /s/<slug>/); auto-generated if omitted |
pages | array | yes | [{path, html}] — e.g. {"path": "/", "html": "<html>...</html>"} |
assets | array | no | [{path, content_base64, content_type?}] — css/js/images/fonts, base64-encoded |
Limits: 20MB per request, 200 pages, 500 assets. Asset types: css, js, png, jpg, jpeg, gif, svg, ico, webp, woff, woff2, ttf, json, xml, txt, pdf. A site with no / page gets a placeholder root.
Response (201)
{
"id": "uuid",
"slug": "my-site",
"title": "My Site",
"url": "https://thin.host/s/my-site/",
"pages": ["/", "/about"],
"assets": ["/css/style.css"],
"page_count": 2,
"asset_count": 1,
"custom_domains": [],
"next_action": "The site is live at ... — share that URL."
}
PATCH /v1/websites/:id_or_slug
Update a website — upserts pages/assets by path and can retitle. Paths not included are left untouched.
Request
{
"title": "My Site v2",
"pages": [{ "path": "/", "html": "<h1>Updated</h1>" }],
"assets": [{ "path": "/css/style.css", "content_base64": "..." }]
}
Response (200)
{ "url": "https://thin.host/s/my-site/", "updated_pages": ["/"], "updated_assets": ["/css/style.css"] }
GET /v1/websites
List your hosted websites, newest first. Supports page and limit (max 100) query parameters.
Response (200)
{
"websites": [ { "id": "...", "slug": "my-site", "url": "https://thin.host/s/my-site/", "page_count": 2 } ],
"total": 1,
"page": 1,
"per_page": 50,
"has_more": false
}
GET /v1/websites/:id_or_slug
Get one website, including its page paths and asset paths.
DELETE /v1/websites/:id_or_slug
Delete a website and all its pages and assets. Not reversible.
Response (200)
{ "deleted": true, "slug": "my-site" }
Domain Status Enum
| Status | Meaning |
|---|---|
pending_payment | Waiting for end user to pay via claim page |
pending_dns | Paid, waiting for DNS records to propagate |
pending_ssl | DNS verified, SSL certificate provisioning |
active | Domain is live with SSL |
failed | Provisioning failed or expired |
released | Domain was released/deleted |