---
title: "Publish a static site"
method: POST
path: "/sites"
tags: ["Sites"]
---

# Publish a static site

`POST /sites`

## Request

- Base URL: `https://api.hypamoon.com/api/v1`
- URL: `https://api.hypamoon.com/api/v1/sites`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://app.hypamoon.com/oauth/authorize`, token URL `https://api.hypamoon.com/oauth/token`, scopes: `sites:write`)

Creates a static site and publishes it immediately at https://<slug>.hypamoon.app. Free, including in test mode. Send every file the browser loads, in full: at least /index.html, plus css, js, images and fonts as separate files or inline. There is no build step, so send the built output, never React or Vite source. Text files (html, css, js, json, txt, xml, svg, webmanifest, md) as plain text. Images (png, jpg, gif, webp, ico), fonts (woff, woff2), PDFs (pdf), video (mp4, webm) and audio (mp3, m4a) preferably as source_url, a public https URL that Hypamoon fetches, and base64-encoded in content only when there is no URL (never for video or audio). The type follows from the extension, and binary files must be that type in their first bytes. A PDF is served inline (Content-Disposition: inline); PDF, video and audio support Range requests (206), and count as one site request per started MB served. A PDF with JavaScript, a /Launch action, a form that submits to another host, or a brand name with a login or payment link is refused (site_blocked). source_url: Hypamoon fetches the file once from a public https URL on port 443 (no IP address or internal host, at most 3 redirects, each checked again; 10 s per file) within the plan's size limits. Its type must match the path: an image or font is recognized by its bytes, text must be UTF-8 and not a web page where a .css or .js was expected. The fetched file then goes through the same checks as one sent with content. A file that cannot be fetched returns 422 with the reason at files.<i>.source_url, and nothing changes. At most 100 files with source_url per request and 60 such requests per project per hour (429 rate_limited). A path without an extension also resolves to <path>.html and <path>/index.html, so /about serves /about.html. Your own /404.html is the not-found page. At most 5 new sites a day on the Free plan (no limit on the number of sites). Free: at most 200 files, 5 MB per file (a PDF 10 MB, video or audio 10 MB) and 50 MB together; with a paid plan or credit 2,000 files, 25 MB per file (a PDF 25 MB, video or audio up to 90 MB via create_upload_urls) and 1 GB together. Keep one request under 4 MB: send a larger file through an upload link (POST /sites/uploads) and name it here with sha256 and bytes instead of content. On the Free plan a project has up to 5 new sites a day, 10 MB of data and 100 MB of files, with phone numbers and payments in test mode only. A call that would go above that fails with 402 plan_limit_reached and next_step "Upgrade to Online"; tell the user, nothing was changed. Online costs EUR 15 a month excl. VAT and is chosen in the portal (Plan). Without slug, one is derived from name. Retrying with the same slug returns 409 slug_taken, never a second site. With version history, send message: what the site is, in one short sentence; it is the first entry in its history. preview is only for PUT /sites/{id}: a new site goes live right away. A static site has no logins or server-side code of its own. To share it with a few people, set password (one shared password, entered once per browser) or make it unlisted; paid access gives each buyer a personal sign-in link. For server-side code and sign-in per user, add an app with auth to the site (POST /apps). Not yet: files over the limits. Every deploy is checked before it goes live: input fields for passwords, PIN codes, card numbers or IBANs, forms that send to another site, hidden iframes, redirects to another site, obfuscated scripts, and login fields next to the name of a bank, government service, parcel or payment service or large platform return 422 site_blocked with the reasons. A new project has a lower limit on deploys per hour and on sites in its first week (deploy_limit_reached, site_limit_reached). With password, every visitor without the password sees a password form on every path (401, not indexed, ten attempts per address per ten minutes), and nothing of the site is cached by the CDN.

## Request body

- SiteInput: A new static site.
  - `slug` string: The address: https://<slug>.hypamoon.app. 3 to 40 characters, lowercase letters, digits and hyphens. Leave it out to derive it from name (with a short suffix if that one is taken).
  - `name` string, required: A name for your own overview, like the business or the page.
  - `files` object[], required: All files of the site: complete, ready for a browser, with at least /index.html. Free: at most 200 files, 5 MB per file (a PDF 10 MB, video or audio 10 MB) and 50 MB together; with a paid plan or credit 2,000 files, 25 MB per file (a PDF 25 MB, video or audio up to 90 MB via create_upload_urls) and 1 GB together.
    - `path` string, required: Where the file lives on the site, like /index.html, /style.css or /img/logo.png. Relative paths (index.html) get a leading /.
    - `content` string: The file itself: plain text for html, css, js, json, txt, xml, svg, webmanifest and md; base64 for png, jpg, gif, webp, ico, woff, woff2, pdf, mp4, webm, mp3 and m4a only when there is no URL for it (prefer source_url; a video or audio file via source_url or an upload, never base64). Leave it out for a file with source_url, or one you uploaded with create_upload_urls (POST /sites/uploads): then send sha256 and bytes.
    - `source_url` string: Instead of content: a public https URL of the file (port 443), for images, fonts, PDFs, video and audio above all. Hypamoon fetches it once (at most 3 redirects, within the size limits of the plan) and checks it like a file sent with content. Its type must match the path (a .jpg path needs a JPEG, a .mp4 path an MP4). Leave encoding out. Use it instead of base64 whenever the file has a URL; never send a video as base64.
    - `encoding` 'utf8' | 'base64': utf8 for text, base64 for images, fonts, PDF, video and audio. Leave it out: it follows from the file type.
    - `content_type` string: Leave it out: it follows from the extension (.html is text/html). Only for a path without a known extension, and only an allowed type.
    - `sha256` string: Only for an uploaded file, instead of content: the SHA-256 of its bytes as 64 lowercase hex characters, as you sent it to create_upload_urls.
    - `bytes` integer: Only for an uploaded file, instead of content: its exact size in bytes.
  - `password` string, nullable: Optional. One shared password for the whole site (4 to 72 characters): visitors enter it once per browser, like password protection on a preview. Not a login system: no accounts, no per-visitor access. null or an empty string removes it. Never returned; the site shows password_protected instead.
  - `media` string[]: Optional. Files from the project's media library (list_media) to put on the site, without uploading them again: folders like "portfolio/" (with subfolders), file ids, or paths like /media/logo.webp. Each file lands at its library path with its 640 version next to it; private originals never go on a site. A file you also send in files wins.
  - `message` string: Optional, but always send it: what changed and why, in one short sentence (up to 200 characters), like a commit message: "New opening hours for the summer" or "Spring menu, the user asked for it". It shows in the version history; without it Hypamoon writes a short summary like "Changed /index.html".

## Response `201`

The site, live at url.

- Site: A static site on hypamoon.app.
  - `id` string, uuid, required: The id of the site. Use it to update or delete the site.
  - `slug` string, required: The first part of the address: https://<slug>.hypamoon.app.
  - `name` string, required: Your name for the site.
  - `url` string, required: The public address of the site, live right away.
  - `files` SiteFile[], required: The files of the site, sorted by path, without their content.
    - `path` string, required: The path of the file on the site, like /index.html.
    - `content_type` string, required: The type it is served with, like text/html.
    - `bytes` integer, required: The size of the file in bytes.
    - `updated_at` string, date-time, required: When the file was last written.
  - `created_at` string, date-time, required: When the site was created.
  - `updated_at` string, date-time, required: When the site or its files last changed.
  - `version` string, required: The version that is live now. Changes on every update; every file on the site carries it as its ETag, so you can check that a page shows this version.
  - `password_protected` boolean, required: true when visitors have to enter the site's password first (set with password on POST /sites or PUT /sites/{id}). The password itself is never returned.
  - `test` boolean, required: true for a site of a test project. It is public all the same.
  - `taken_offline` object, nullable, required: null while the site is live. Set when Hypamoon took the site offline because of abuse: visitors get a page that says it is unavailable, the files are kept, and PUT and DELETE return 403 site_offline. To object, the owner replies to the email or writes to hello@hypamoon.com.
    - `at` string, date-time, required: When Hypamoon took the site offline.
    - `reason` string, required: Why, in the words the owner also received by email.
  - `warnings` union[]: Only in the answer of a deploy, and only when there is something to tell: files a deploy without keep_existing removed, and build hints. Warnings never block a deploy.
    - union
      - object
        - `code` 'files_removed', required: A deploy without keep_existing removed files that were on the site.
        - `message` string, required: What happened and what to do if it was not intended.
        - `paths` string[], required: The paths that are gone.
      - object
        - `code` 'build_hint', required: Something on the site looks like it works but does not: a form that saves nowhere, localStorage used as a database, a password checked in JavaScript, a button to # or javascript:, a cart without a checkout, test wording on the live project, or a map from a service that needs a key the site does not have. Advice only: the deploy succeeded.
        - `hint` string, required: Which hint, like form_no_target, local_storage_db or cart_without_checkout.
        - `message` string, required: What does not work, in plain words.
        - `paths` string[], required: The files the hint is about.
        - `next_step` string, required: What to do next, with the tool to use.

## Other responses

- `202`: Four-eyes approval is on: the deploy waits for another owner or builder. Nothing has changed on the live site yet.
- `400`: Bad Request: invalid_json.
- `401`: Unauthorized: unauthorized, missing_api_key, invalid_api_key, api_key_revoked, project_deleted.
- `402`: Payment Required: plan_limit_reached.
- `403`: Forbidden: project_not_enabled, key_mode_mismatch, missing_scope, verification_required, phone_verification_required.
- `409`: Conflict: slug_taken, plan_limit_reached.
- `422`: Unprocessable Content: validation_failed, site_too_large, upload_not_received, slug_reserved, site_limit_reached, site_blocked.
- `429`: Too Many Requests: rate_limited, deploy_limit_reached.
- `500`: Internal Server Error: internal_error.
- `503`: Service Unavailable: service_unavailable.

---

[API](https://skmtc.dev/hypamoon/apis/hypamoon-api.md) · [All operations](https://skmtc.dev/hypamoon/apis/hypamoon-api/llms.txt) · [OpenAPI document](https://skmtc.dev/hypamoon/apis/hypamoon-api/revisions/9f0b90004c06?raw)
