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. For TESTING (e.g. OTP flows) you can send from your free *.host67.com subdomain with zero DNS setup — enable it under Email — but it's a sandbox: ~50/day, <=5 recipients per message, and every message gets a "test address" footer, so it's not usable for production. Verify your own domain for real sending. 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.