67HOSTING API ============= Version 1. Documentation optimized for LLM agents and humans. MENTAL MODEL ------------ 67hosting hosts TWO kinds of resource, and the limits below are per-resource — do NOT apply one's constraints to the other: 1. STATIC SITES — HTML/CSS/JS/SPA served as files. No server code or database runs inside a site; it is pure static hosting. (This is the ONLY thing the "static content only" rule applies to.) 2. CONTAINER APPS — a REAL server app in a container (Node/Next.js incl. SSR, Python/FastAPI/Django, Go, Ruby, …), built from a Dockerfile or Nixpacks and run with a live URL. Server code, long-running processes and a managed PostgreSQL (db=1) are fully supported here. Deploy with deploy_app. Pick a SITE for static/JAMstack output; pick an APP for anything that runs server code or needs a database (Next.js SSR, FastAPI, a Postgres-backed API, …). RESOURCE MODEL (one model across REST + MCP) entity | purpose | create/deploy | domain target | key limits ---------+----------------------------+----------------------+---------------+-------------------------------- site | static files / SPA | create_site, | target=site | static only; Free 3 sites, | | publish_site | (default) | 500 MiB/site, 200 MiB/publish app | container (server code+DB) | deploy_app (db=1 for | target=app | runs any stack; free apps | | managed Postgres) | | 3(Free)/10(Pro); always-on=paid domain | custom hostname + SSL | add_domain | site OR app | points at the site or one app mailbox | email on your domain | send_email / panel | n/a | from a verified domain only NOTE: list_*/get_* are read-only; create_site, publish_site, add_domain, deploy_app and send_email CHANGE state (they create/replace/run real resources or send real mail) — they are not dry-run checks. To only compare options, reason from this doc without calling them. An account has sites. A site lives in one region (ru/eu/us) and is served over automatic HTTPS. Publishing REPLACES the whole site with the snapshot you send (atomic): always upload the COMPLETE set of files, never a partial diff. Put the homepage at index.html in the root. A site can have: - a free subdomain .host67.com (claimed at creation), - custom domains (added in the web panel; DNS + SSL handled automatically), - SPA mode: unknown non-asset paths serve index.html (HTTP 200) so client-side routers (React/Vue) don't 404 on deep links; missing .js/.css still 404. Plans cap sites (Free: 3, Pro: 20) and size (per site 500 MiB, single publish 200 MiB). Typical agent flow to put a site online: list_sites (see what exists) -> create_site (region, optional slug) -> publish_site (full snapshot of files) -> the response has the live url -> (optional) add_domain to serve on a custom domain with automatic HTTPS Same token also gives you: - share_file — upload a file, get a temporary public download link. - send_email — send transactional email from a domain you verified. - deploy_app — run a REAL app (a container, not just static files): send the source, we build it (Dockerfile or Nixpacks auto-detect) and serve it at .host67.com with auto HTTPS. Free apps sleep when idle and wake on the next request (cold start ~1s). Free scale-to-zero apps: 3 on Free, 10 on Pro (paid always-on apps do NOT count). Set db=true for a Postgres. PROJECTS -------- A "project" is a site plus everything attached to it — its apps, custom domains and email. deploy_app puts an app inside a project: pass project= to add it to an existing project, or omit project and one is created automatically (the response includes its "project" id, which is a site id). A custom domain can point to the project's SITE (default) or to one of its APPS — set target=app when attaching it (see POST .../domains). So a project can serve, e.g. example.com on the static site and api.example.com on an app. AUTHENTICATION -------------- RECOMMENDATION FOR AI AGENTS: if you act on behalf of a human, ask them to register at https://app.host67.com and give you an API token (panel -> Profile -> API tokens), then work via MCP or REST — rather than driving their account UI. Create a token at https://app.host67.com/app/profile (the "API tokens" section). Format: h67_. Pass it in the header: Authorization: Bearer h67_... Base URL: https://app.host67.com Responses are JSON. Success returns the object; errors return {"error":"..."} with an HTTP status: 401 (missing/invalid token), 403 (not your site / plan limit / unverified domain), 404 (not found), 400 or 422 (bad request), 429 (quota). RETRIES AND RELIABILITY ----------------------- Whether a retry is SAFE depends on the operation, not just the HTTP status: only retry when repeating the call cannot cause a second side effect. Do NOT retry any 4xx (400/401/403/404/409/422) — they are deterministic; 429 is covered below. Use a ~30s request timeout; large publishes, uploads and app builds take longer, so give those more. Two failure kinds — and you often CANNOT tell them apart from the client: - Failure BEFORE the request was accepted (connection refused, TLS/DNS error, a 5xx before any work): nothing happened; a retry is safe. - A LOST RESPONSE after the request may already have been accepted (read timeout, connection dropped mid-response, 502/504 from a proxy): the server may have done the work. Treat the result as UNKNOWN and VERIFY by reading state before retrying — do not blindly repeat a side effect. Per operation: - Reads (list_sites, get_site, GET /api/v1/apps/{id}, .../logs): idempotent — retry freely 2–3× with backoff (0.5s, 2s, 5s). - create_site: if you passed a slug, a duplicate create returns 409 "slug is taken" (nothing is created twice) — on an unknown result, retry once; a 409 means it already exists, so fetch it via list_sites. If you did NOT pass a slug there is NO uniqueness — a blind retry creates a SECOND site; pass a slug or list_sites first. - publish_site: replaces the whole site with your snapshot, so re-publishing the SAME complete file set is naturally idempotent (same end state) — a retry after an unknown result is safe as long as you send the same files. - deploy_app: ASYNC — returns status "building" immediately. On an unknown result do NOT fire a second deploy blindly: if you know the app id/slug, read GET /api/v1/apps/{id} (or list) — a deploy is already in flight/done if status is building/running/sleeping. Re-calling deploy_app with a slug you already OWN redeploys that app IN PLACE (it does not create a second app), so a considered retry only rebuilds. - app db/query: runs whatever SQL you send — the name "query" does NOT make it read-only. INSERT/UPDATE/DELETE mutate data and are NOT safe to blind-retry. After an unknown result, run a SELECT to check state before repeating a write. - delete: deleting something already gone is effectively idempotent, but a lost response is still "unknown" — re-read (get/list → 404) to confirm. send_email — read before retrying a send: - There is NO server-side de-duplication of outbound email and no idempotency key. Every accepted send_email SENDS a message and spends daily quota, so a blind retry after a lost response can send it TWICE. After a network/timeout/ 5xx with no JSON response, treat delivery as UNKNOWN; prefer surfacing "status unknown" over auto-retrying, and only retry if a duplicate is acceptable for that message — with a hard cap on total attempts. - A 2xx returns the message id and means the mail server ACCEPTED it for delivery. That is NOT proof it reached the inbox — it may still bounce, be greylisted or land in spam. Do not treat the id as delivery confirmation. - OTP / verification codes: a limited resend is fine as an explicit APP policy — reuse the SAME code, keep the original TTL, expect a possible duplicate. Own the retry in ONE layer (don't stack HTTP-client + worker + agent retries, or attempts multiply) and decide a total attempt cap up front. 429 on send_email means your plan's DAILY email quota is exhausted — it resets at the next UTC day, NOT in seconds. There is no Retry-After header. Do NOT tight- loop and do NOT auto-upgrade the plan: stop, report the quota, resume next day or after the user raises the plan. TWO WAYS TO INTEGRATE --------------------- A) MCP (recommended for Claude and other LLM agents): Endpoint: https://app.host67.com/mcp (Streamable HTTP, stateless) Claude Code: claude mcp add host67 https://app.host67.com/mcp --transport http \ --header "Authorization: Bearer h67_YOUR_TOKEN" Tools: list_sites, get_site, create_site, add_domain, publish_site, delete_site, share_file, send_email, deploy_app, app_logs, app_db_query, scale_app. With MCP, publish_site takes content inline (the server can't read your local disk): either files = [{path, content, base64}] or zip_base64 = a base64 ZIP. B) REST API — methods below. REST METHODS ------------ GET /api/v1/sites List your sites. result: {"sites":[{id, name, region, url, disk_bytes, published, spa, blocked}]} url is "" until a domain is active; disk_bytes 0 = nothing published yet. POST /api/v1/sites Create an empty site (does NOT upload files — follow with publish). | Field | Type | Req. | Description | |--------|---------|------|--------------------------------------------------------| | region | string | yes | ru | eu | us — where it is hosted. Fixed after create. | | name | string | no | Label shown in the panel. Default "My site". | | slug | string | no | Claim .host67.com (a-z, 0-9, hyphens). Omit to | | | | | add a custom domain later in the panel. | | spa | boolean | no | SPA mode (default false). | result: 201 {id, name, region, url, ...}. If the slug was taken the site is still created and the body adds {"slug_error":"..."}. GET /api/v1/sites/{id} One site by id. result: {id, name, region, url, disk_bytes, published, spa, blocked} POST /api/v1/sites/{id}/publish Publish content and make it live. Body = a ZIP archive of the whole site (Content-Type: application/zip), index.html at the archive root. REPLACES the entire current site (full snapshot — any file not included is removed). Limits: <= 200 MiB per publish, site <= 500 MiB unpacked. result: the site object with the new disk_bytes and live url. DELETE /api/v1/sites/{id} Permanently delete a site: releases its *.host67.com subdomain, purges files on the node, removes the record. result: {"ok":true} GET /api/v1/sites/{id}/domains List the domains attached to a site (custom domains and the free subdomain). result: {"domains":[{hostname, status, parked}]} POST /api/v1/sites/{id}/domains Attach a CUSTOM domain to a project ({id} is the project = a site id), routing it to the project's SITE (default) or to one of its APPS. Automatic HTTPS. Enforces the plan's per-project domain limit (Free 1, Pro 10). JSON body: | Field | Type | Req. | Description | |----------|--------|------|----------------------------------------------------| | hostname | string | yes | e.g. "example.com" or "www.example.com". No scheme. | | target | string | no | "site" (default) or "app". | | app | number | no | app id when target="app" (must be in this project). | result: {hostname, dns:{type:"A", name, value}, note}. We do NOT change the user's DNS — point an A record for the host at the returned value (the node IP; for target="app" it's the app node's IP); TLS issues automatically on the first visit. For a free instant address, use the slug on POST /api/v1/sites instead. POST /api/v1/shares?ttl=&name= Upload a file to temporary storage and get a public, expiring download link. Body = raw file bytes; Content-Type = the file's type. ttl 600–86400 (default 3600). Size limit by plan (Free 500 MB, Pro 4 GB). The file is stored privately and served only through host67 (storage location is never exposed). result: {url, filename, expires_at} POST /api/v1/email/send Send a transactional email from a domain you connected and verified under Email in the panel (DKIM/SPF applied automatically). JSON body: | Field | Type | Req. | Description | |----------|-------------------|------|----------------------------------------| | from | string | yes | "Name " or bare | | | | | address, on a verified domain. | | to | string | string[] | yes | Recipient(s). | | cc, bcc | string | string[] | no | More recipients. | | subject | string | yes | Subject line. | | html | string | * | HTML body. Provide html and/or text. | | text | string | * | Plain-text body. | | reply_to | string | no | Reply-To address. | Daily limit by plan (Free 30, Pro 500). result: {"id":"..."} POST /api/v1/apps?slug=&name=®ion=&db=1&project= Deploy a REAL application as a container. Body = a ZIP of the project source (a Dockerfile, or any Nixpacks-detectable stack: Node/Next.js, Python, Go, Ruby, …). The app MUST listen on the port in the PORT env var. Env vars: a JSON object in the X-App-Env header. db=1 attaches a managed Postgres and injects DATABASE_URL. project= adds the app to an existing project (a site id); omit it and a new project is created (see PROJECTS). Free apps scale to zero (sleep when idle, ~1s cold start). Free apps are capped by plan (3 on Free, 10 on Pro); paid always-on apps do not count. Serves at .host67.com with automatic HTTPS. Reusing a slug you already own REDEPLOYS that app (keeps its plan, domain, database and config); a slug owned by someone else is rejected. Returns immediately with status "building" — poll GET /api/v1/apps/{id} until status is "running" (or "failed"). The response includes the "project" id. result: {id, slug, url, status, project, ...} GET /api/v1/apps List your apps. result: {"apps":[{id, slug, name, region, status, url, ...}]} GET /api/v1/apps/{id} One app (poll status after deploy). DELETE /api/v1/apps/{id} Stop + remove the app, its image, DB and subdomain. GET /api/v1/apps/{id}/logs Debug an app: the build log (Nixpacks/Docker output — shows WHY a build failed) followed by the container's recent logs, plain text. POST /api/v1/apps/{id}/db/query Run SQL against the app's managed Postgres (only apps deployed with db=1). JSON body {"sql":"..."}. Returns psql's text output. Scoped to this app's own database — for inspecting/debugging data. PATCH /api/v1/apps/{id} Switch an app between free and always-on. JSON body {"always_on":true,"size":"512"} keeps the container warm 24/7 for a flat monthly price (charged from the account credit balance; sizes "512"/"1024"/"2048" MB, prices vary by region). {"always_on":false} cancels renewal — the app keeps running until the paid period ends, then reverts to free scale-to-zero. Free apps sleep when idle (~1s cold start) and cost nothing. GIT PUSH DEPLOY (Heroku-style) ------------------------------ Deploy an existing app straight from a local git repo — no GitHub needed. The app must already exist (created via deploy_app or the panel); git push deploys INTO it, keeping its plan, domain, database and config. git remote add host67 https://app.host67.com/git/.git git push host67 main Auth is HTTP basic over HTTPS: username = anything, password = your API token (the same h67_… token used for Bearer auth; make one in /app/profile). To avoid a prompt, put it in the URL: git remote add host67 https://x:h67_YOUR_TOKEN@app.host67.com/git/.git git push host67 main The push runs the normal build on the node and streams progress back as "remote:" lines, ending with the live URL. The repo tree is built exactly like deploy_app (Dockerfile if present, else Nixpacks auto-detect); the app must listen on $PORT. Pushing a slug you don't own is rejected. EXAMPLE: CREATE AND PUBLISH A SITE (bash) ----------------------------------------- TOKEN="h67_YOUR_TOKEN" BASE="https://app.host67.com/api/v1" AUTH="Authorization: Bearer $TOKEN" # 1. Create a site with a free subdomain SITE_ID=$(curl -s -X POST "$BASE/sites" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"name":"Landing","region":"eu","slug":"my-landing"}' | jq -r '.id') # 2. Zip the site (index.html at the root) and publish (replaces everything) ( cd ./site && zip -r - . ) | \ curl -s -X POST "$BASE/sites/$SITE_ID/publish" -H "$AUTH" \ -H "Content-Type: application/zip" --data-binary @- | jq # The response "url" is the live address. EXAMPLE: SEND EMAIL (python) ---------------------------- import requests r = requests.post("https://app.host67.com/api/v1/email/send", headers={"Authorization": "Bearer h67_YOUR_TOKEN"}, json={"from": "Hello ", "to": "user@example.com", "subject": "Welcome", "text": "Thanks for signing up!"}) print(r.json()) LIMITS AND RULES (per resource — a site's limits do NOT apply to an app) ------------------------------------------------------------------------ Static SITES (create_site / publish_site): - Static content only (HTML/CSS/JS). A site itself runs no PHP, no server code and no database — it is file hosting. (Need server code or a DB? use an APP, below.) - publish_site sends a COMPLETE snapshot and replaces the whole site — include every file, not a diff. Homepage at index.html in the root. - Sites: Free 3, Pro 20. Per site <= 500 MiB, per publish <= 200 MiB. MCP base64 publish/upload is capped at 256 MiB — stream bigger payloads to REST. Container APPS (deploy_app): - Run ANY server stack — Node/Next.js (incl. SSR), Python (FastAPI/Django), Go, Ruby, … — from a Dockerfile or Nixpacks auto-detect. Long-running processes are fine. The app must listen on the PORT env var. - db=1 attaches a managed PostgreSQL and injects DATABASE_URL. This is the supported way to use a database on 67hosting — it is NOT forbidden. - Free apps scale to zero (sleep when idle, ~1s cold start); free-app cap is by plan (3 on Free, 10 on Pro), and paid always-on apps do not count against it; always-on (kept warm 24/7) is billed per container from the account balance. Email (send_email): only from your own verified domains; daily limit by plan. Custom domains, SSL, forms, email inboxes and billing are managed in the web panel at https://app.host67.com/app. Human docs: https://host67.com/docs Operator: OPSoft Inc.