---
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 <upload_url>` 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": "<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.
