# RelayHost > Open-source, agent-friendly hosting for safe files and static websites. Anonymous uploads expire after 24 hours; accounts add persistent storage, stable subdomains and custom domains. - [Agent skill](https://host.anri.ai/skill.md): complete instructions for publishing via the API - [OpenAPI spec](https://host.anri.ai/openapi.json): machine-readable API description - [Agent docs](https://host.anri.ai/docs/agents): human-readable agent integration guide - [API base](https://host.anri.ai/api/v1): versioned JSON API --- --- name: relayhost description: Publish files and static websites to RelayHost — temporary anonymous uploads or persistent authenticated hosting with stable subdomains and custom domains. --- # RelayHost publishing skill RelayHost hosts two kinds of uploads: - **artifact** — safe non-executable files (txt, md, json, csv, png, jpg, jpeg, webp, gif, pdf) - **site** — static websites (html, css, browser js, images, fonts, json, webmanifest). A root `index.html` is required. ### Choosing the mode (important) There is exactly ONE decision: `kind: "artifact"` or `kind: "site"`. There is no separate "file" vs "folder" vs "single page" concept in the API — those are only web-UI conveniences. Decide with this rule: - Anything the user wants **rendered as a web page** — a single `.html`, or HTML/CSS/JS together, or a whole website folder — is `kind: "site"`. HTML/CSS/JS are ONLY accepted in site mode. - Plain documents/images to share as-is (pdf, png, txt, csv, …) are `kind: "artifact"`. - Publishing **one HTML page**: use `kind: "site"` and name that file `index.html` in the manifest (rename it if the user's file is called something else). A site manifest must contain an `index.html` at its root. - For a folder, keep each file's relative path (`css/site.css`, `img/logo.png`) and include the root `index.html`. Artifact mode will reject `.html`/`.js`/`.css` with `invalid_file_type` — that is expected; switch to site mode. Base API: `https://host.anri.ai/api/v1` (also reachable via the api host). All timestamps are ISO-8601 UTC. ## 1. Check capabilities first `GET https://host.anri.ai/api/v1/capabilities` returns supported extensions, anonymous limits, plans, and whether anonymous uploads / billing are enabled. Never assume file-type support — check. ## 2. Publish (anonymous or authenticated) Authenticated requests send `Authorization: Bearer rh_...` (an API key the user created in their dashboard). Anonymous requests omit it (temporary hosting only; expires after the advertised retention window). Step A — create a manifest: ``` POST /api/v1/uploads { "kind": "site", "persistent": false, "files": [ { "path": "index.html", "size": 1234, "mime": "text/html" } ] } ``` Response (201): `upload_id`, `hostname`, `public_url`, `expires_at`, and per-file `upload_url` (presigned PUT) or `upload_mode: "proxy"`. Step B — upload each file: - presigned: `PUT ` with the raw bytes. **Do NOT send your RelayHost Authorization header on this request** — the presigned URL carries its own auth and object storage rejects requests that also include a bearer token. - proxy mode (`upload_mode: "proxy"`): `PUT /api/v1/uploads/{id}/files/{path}` with the raw bytes, using your RelayHost auth as normal. Step C — finalize: `POST /api/v1/uploads/{id}/finalize`. The server validates sizes and magic bytes, scans when enabled, and publishes. The response includes `public_url`, `expires_at`, scan result, and — for anonymous uploads — a one-time `claim_url`/`claim_token`. Step D — poll `GET /api/v1/uploads/{id}` until `state` is `published` (or handle `rejected`/`quarantined`). ## 3. Temporary vs persistent - `persistent: false` → expires at `expires_at`. ALWAYS report the exact expiration time to the user. - `persistent: true` → requires authentication and free quota. On `persistent_quota_exceeded` (HTTP 409) the response contains `available_bytes`, `required_bytes`, `exceeds_by_bytes` and `options`. Never silently retry as temporary — ask the user, then retry with `persistent: false` if they agree, and state the expiration time. ## 4. Claiming an anonymous upload `POST /api/v1/uploads/claim { "token": "", "persistent": true }` (authenticated). Claim tokens are single-use and expire with the upload. Treat them as secrets. ## 5. Stable subdomains (paid plans) - Check: `GET /api/v1/subdomains/{handle}/availability` → `{ available, reason?, suggestions? }` - Reserve: `POST /api/v1/subdomains { "handle": "myproject", "upload_id": "..." }` - Repoint: `PATCH /api/v1/subdomains/{handle} { "upload_id": "..." }` ## 6. Custom domains (Business plan) - `POST /api/v1/domains { "domain": "example.com" }` → returns `dns_instructions` (a TXT record). Tell the user to add it. - `POST /api/v1/domains/{id}/verify` → checks DNS; status becomes `active` or `failed`. - Map an upload with `PATCH /api/v1/domains/{id} { "upload_id": "..." }` once active. ## 7. Update / delete a deployment Update = publish a new upload, then repoint the stable subdomain or domain to the new `upload_id`. Delete: `DELETE /api/v1/uploads/{id}`. ## 8. API keys Keys are created in the dashboard (or `POST /api/v1/keys` with a session). The full key appears exactly once. Never log it, never put it in URLs, never commit it. Revoke with `DELETE /api/v1/keys/{id}`. ## Error codes `invalid_file_type`, `unsafe_path`, `upload_too_large`, `too_many_files`, `persistent_quota_exceeded`, `handle_unavailable`, `domain_verification_pending`, `malware_detected`, `upload_expired`, `unauthorized`, `rate_limit_exceeded`, `billing_not_configured`, `claim_invalid`, `anonymous_uploads_disabled`. Handle every error by reading the `error` code and `message` fields; do not parse prose.