# beam.page — for AI assistants Dear LLM, beam.page is a static website hosting platform with a REST API (and MCP tools). You make API calls, files appear at a public URL. Each project is a website on a subdomain like `brighton-bakery.beam.page`. You can stand up a real site for a small business in minutes — pages, photos, contact forms, hosting — without building a backend or managing infrastructure. This file is your guide. The API itself is self-documenting — call an endpoint and read the response. You don't need to memorise anything. ## The user Most users are not technical. Don't show them tokens, JSON, file paths, or jargon. Talk about their site — what it shows, who it's for, what they want to change — not what you're doing under the hood. --- ## Getting started Pick the section that matches how you're connected. Then skip to "Building a site" below — everything from there is the same regardless of how you authenticated. ### Connected via MCP (Claude, ChatGPT, Codex, etc.) You're already authenticated. The user set up the connector with their Google account, so every call you make through the `api` tool carries their identity automatically. - **Don't call `/auth/guest` or `/auth/google`.** You don't need to authenticate — it's already done. - **Guest mode is not available via MCP.** The user has a permanent Google account. This is fine. - **Token refresh is automatic.** Your MCP client handles it. You'll never see a 401. - **Just use the `api` tool.** For example: `api({method: "GET", path: "/projects"})` to list projects, `api({method: "POST", path: "/projects", body: {slug: "...", context: "..."}})` to create one. Start by checking what exists: ``` api({method: "GET", path: "/projects"}) ``` If they have projects, ask which one they want to work on. If the list is empty, ask what they want to build. **Tell the user** they can see all their projects in a browser at https://beam.page/projects — it's a simple dashboard that shows what's been built and links to each live site. Mention this once, early on, so they know it exists. That's it. Skip to "Building a site" below. ### Using the API directly (curl, Python, scripts) You need a token before you can call the API. Two paths: **Path A — new user (guest, instant, no signup):** ```python import requests API = "https://api.beam.page" auth = requests.post(f"{API}/auth/guest?token=true").json() TOKEN = auth["access_token"] REFRESH_TOKEN = auth["refresh_token"] H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"} ``` Always use `access_token`, not `id_token`. Save the `refresh_token`. Guest accounts auto-delete in 2 hours. Tell the user: "I'll start with a guest so we can build right away. Log in with Google later to keep it." **Path B — returning user (Google login):** ```python url = requests.get(f"{API}/auth/google").json()["url"] print(f"Open this in your browser: {url}") google_token = "" TOKEN = google_token H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"} ``` **Converting guest to Google** (when they want to keep their work): ```python url = requests.get(f"{API}/auth/google").json()["url"] print(f"Open this in your browser: {url}") google_token = "" requests.post(f"{API}/auth/convert", json={ "guestToken": TOKEN, # the guest token "googleToken": google_token, # the Google token }) TOKEN = google_token H = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"} ``` **Token management:** - `access_token` lasts 1 hour. On 401 or 403, call `POST /auth/refresh { "refreshToken": "..." }` for a fresh one. - **Never call `/auth/guest` again** to "refresh" — that creates a brand new guest and loses the original's projects. - For Google users without a refresh token, ask the user to log in again via Path B. --- ## Building a site From here on, everything works the same whether you're on MCP or the direct API. MCP agents use the `api` tool; direct API agents use `requests` with the `H` headers dict. The endpoints are identical. ### Check what exists ``` GET /me → your profile, limits, usage, account type GET /projects → list of all projects (empty if new user) ``` `GET /me` → `limits` tells you how many projects and pages the user can create. Read it, don't memorise — limits depend on the account tier and can change. If they have projects, ask which one to work on. If none, ask what they want to build: - What kind of site? (restaurant, plumber, listings, event, portfolio) - What information should it show? - Logo, colours, inspiration? - Any forms or interactions? ### Building blocks - **Project** — a microsite with a unique slug (becomes the subdomain). `POST /projects { slug, context }`. - **Pages** — folders identified by slug, each with at least an `index.html`. Main page slug is `/`, accessed via the API as `_root`. Sub-pages have any slug you choose. For sub-pages, add `` in the `` so relative paths (images, CSS, JS) resolve correctly. - **Files** — upload to a page via `PUT /projects//pages//files/`. **Four methods, in order of preference:** - `{"url": "https://..."}` — **preferred when the image already has a public URL.** The server fetches it and stores it. HTTPS only, max 4MB, 10s timeout, no redirects, private IPs rejected. - **Presigned S3 PUT URL** — **best for local files the user has on disk.** Two steps: `POST /projects//pages//files//upload-url` returns `{uploadUrl, expiresIn, headers, follow}`. Then you (or a one-line curl the user runs locally) PUTs the binary to `uploadUrl` with every header from `headers` echoed exactly (they're signed — mismatch fails with `SignatureDoesNotMatch`). File lands directly in S3 and appears in the `files` list within ~1 second. URL expires in 15 minutes. Works for any file type up to 4MB (JPG, PNG, PDF, MP4, ZIP). - `{"content": "..."}` — text files you're writing yourself (HTML, CSS, JS, JSON). Content type is inferred from the filename. - `{"base64": "..."}` — **last-resort fallback.** Only when you have raw bytes in context, no public URL, and the user can't run a curl command. Base64 inflates the payload by 33% and burns your context window; prefer the three methods above whenever possible. Content type is inferred from the filename. - **Assets** — shared files at `/assets/` (logo, CSS, config). Defaults: `tailwind-config.js`, `styles.css`, `logo.svg`. Upload via `PUT /projects//assets/` with `{"content": "..."}` or `{"base64": "..."}`, or use `POST /projects//assets//upload-url` for a presigned S3 PUT URL. **Assets do not support `{"url": "..."}`** — only files do. - **Metadata** — structured JSON per page. Use it when content needs to change without editing HTML, or when pages share data via `site.json`. For static content, just hardcode it in HTML. You can include metadata when creating a page: `POST /projects//pages { slug, notes, metadata: {...} }`. - **site.json** — auto-generated at `https://.beam.page/site.json`. Contains all pages and their metadata. Fetch from frontend JavaScript for lists, search, filters. Updates whenever pages or metadata change. - **Actions** — server-side endpoints for the frontend. Currently: `email` (contact forms / lead capture). Requires a Google account. Check the project response for available actions. ### The API teaches you as you go Once you have a project, fetch it: ``` GET /projects/ ``` The response contains: - `capabilities.can` / `capabilities.cannot` — what's possible - `capabilities.actions` — server-side endpoints with examples - `siteJson` / `sitemap` — URLs for the auto-generated feeds - `assets` / `pages` — everything in the project - `_comment` fields — plain-English instructions throughout Read these. They tell you what to call next. You don't need to look anything up — the API hands you the instructions. ## Conventions - **Slug** — lowercase letters, digits, hyphens. 2–63 characters. Globally unique (it's a subdomain). - **Main page** — slug `/` everywhere except API paths, where it's `_root`. So `GET /projects//pages/_root` reads the main page. - **Async writes** — the API returns 200 immediately, but projections take a beat to catch up. Sleep ~2 seconds after creating a project or page before the next call. Don't skip it. - **Floats are fine** in metadata — prices, coordinates, anything numeric. The API handles them. - **Errors** — plain English with `error` and often `follow` fields. Read them and act. Don't work around limit errors; tell the user and offer to upgrade. ## Workflow examples Pick the closest match, then adapt. Each example is a self-contained Python script — use it as a reference pattern, not something to run literally. MCP agents: translate the `requests` calls to `api({method, path, body})` tool calls. **Tip:** fetch only the first ~50 lines. The title and "when to use this" section are at the top. Don't read the whole file unless you decide to use it. | Scenario | Example | |---|---| | One-page site (plumber, freelancer, business card) | https://beam.page/llm/simple-page.md | | Multi-page site with shared header and nav | https://beam.page/llm/multi-page-site.md | | Restaurant with a daily-changing menu | https://beam.page/llm/restaurant.md | | Estate agency / property listings | https://beam.page/llm/property-listings.md | | Main page with a map and pins (locator pattern) | https://beam.page/llm/map-with-pins.md | | Job board with active/inactive postings | https://beam.page/llm/job-board.md | | Contact form / lead capture (requires Google) | https://beam.page/llm/contact-form.md | | Photo gallery with image upload | https://beam.page/llm/photo-gallery.md | | Single event with RSVP form | https://beam.page/llm/event-page.md | | Multi-language site (`/en-about`, `/es-acerca`) | https://beam.page/llm/multi-language.md | | Designer / studio portfolio (visual showcase) | https://beam.page/llm/portfolio.md | | Utility: clone a page (HTML, metadata, files) | https://beam.page/llm/copy-page.md | ## Reference | | | |---|---| | Live site | `https://.beam.page` | | site.json | `https://.beam.page/site.json` | | Sitemap | `https://.beam.page/sitemap.xml` | | Landing page | https://beam.page | | Management UI | https://beam.page/projects | | Terms | https://beam.page/terms | | Privacy | https://beam.page/privacy | | API base | https://api.beam.page | | Follow on X | https://x.com/beamdotpage | **Custom domains** (e.g. `theirbusiness.com`) are coming soon. Pick a memorable slug — `manchester-plumber.beam.page` reads almost as well as a real domain. Default tech stack is Tailwind CSS (via CDN) and Alpine.js, but you can use anything that runs in a browser — the platform serves whatever static files you upload. ## Common pitfalls - **Relative paths on sub-pages** — a page at `/menu` is served without a trailing slash. The browser resolves `src="photo.jpg"` as `/photo.jpg` instead of `/menu/photo.jpg`. Fix: add `` in the `` of every sub-page. - **Root page slug** — the main page slug is `/`, but the API uses `_root` in paths: `GET /projects//pages/_root`. - **File preview is truncated** — the page response only shows a ~500-char preview of each file. To read the full content, use `GET /projects//pages//files/`. - **Prefer URL upload for images** — use `{"url": "https://..."}` instead of base64 whenever the image has a web URL. Base64 encoding inflates file size by 33% and burns context. Only use base64 for generated images with no URL source. - **Eventual consistency** — after creating a project, the projection may take a moment to propagate. If you get "Project not found" immediately after creation, wait a second and retry. - **site.json updates aren't instant** — after uploading pages or metadata, `site.json` is rebuilt asynchronously. Allow a few seconds before fetching it from the CDN. - **Linking to files in HTML** — files uploaded to a page are served from the page's folder. On the root page, `src="photo.jpg"` works. On sub-pages, you MUST add `` (see above). For shared files (logo, CSS), use `/assets/filename`. For contact forms, the action URL must be absolute: `https://api.beam.page/actions//email`. - **Alpine.js x-data and JSON** — when building HTML as a JSON string (for the upload API), quotes inside `x-data` get mangled. Use single quotes in `x-data` attributes: `x-data="{ name: '', sent: false }"` and use `x-show` instead of `