Moseik platform docs

Moseik hosts small-business websites that are fast, accessible, SEO-strong, and safe to edit — including by AI assistants. This is the public documentation: any person or AI can read it here (rendered) or fetch the raw Markdown by appending .md to any page URL.

Building a website for your human? You can do all of it, free for 180 days and with no key: connect to https://api.moseik.app/mcp and follow Getting started. Already connected to a site? Read Authoring pages before you write.

How the platform works

Don't build these — the platform already has them

Read this list before you start building — by the time you'd look something up, you'll have written it yourself. Roughly two-thirds of it exists because the alternative is either wrong or physically blocked: every page serves under a same-origin Content-Security-Policy, so a third-party script, font or fetch fails to load.

<!-- BEGIN GENERATED: capability-table (edit the domain file under apps/platform/src/core/capability-inventory/, then run: pnpm tsx tools/gen-capability-table.ts) --> <!-- prettier-ignore -->

Instead of…UseWhere
waiting to be told what to build, or hearing about a client's Hivoma form request second-handGET /v1/integration-requests — claim one, do the page work, report the route backAPI
a local copy you push over the top, or net-size checks to catch bad overwritesnothing — a write deleting >20 live lines is refused (write.destructive_overwrite) with the count and what would be lost; re-read and mergeAPI
a lease, a 409, or a guess to find out whether someone else edited since you syncedlastWrittenBy / lastWrittenAt on every content read — and a write that REMOVES another credential's lines is refused (write.cross_writer_overwrite) naming them; a merged copy passes free, and so does one that merely restructures their linesAPI
re-reading every doc each run, or trusting a cached copy that went stale silentlyGET /docs/versions.json and the ETag on /v1/manifest — both content-hashed, so a conditional read is 304 when nothing movedAPI
sleeping after a stylesheet publish, or diffing the served CSS against your local file to guess whether the re-render landedX-Mk-Stylesheet-Version on the served page — compare it to the publishedVersionId the PUT returnedAPI
writing CSS for a component state you cannot make happen, and shipping it having never rendered?mkstate=agents:unavailable (also :empty, :nophoto, :unconfigured, and listings:error) on any page URLauthoring
a <div role="dialog"> plus island code for the focus trap, Esc, the dim and focus restore<dialog> + showModal() — the browser supplies all four, and ::backdrop styles the dimauthoring
hand-writing a <form> and finding somewhere to POST it<mk-component name="form"> → the leads pipeline, spam-filteredauthoring
asking a client to copy the enquirer's address out of a notification, or emailing a manager separately so they see enquiriesnothing for the reply — Reply-To is already the enquirer. For a copy: PUT /v1/site { notifyCc } / { notifyBcc }, or data-notify-cc / data-notify-bcc per formAPI
retyping every agent's details onto their page, and guessing which routing key is theirsone /agents/:slug template over the synced roster collection, which keeps every agent page current; GET /v1/roster reads the same records live. Setting a brokerage up start to finish is one ordered procedure in authoringAPI
copying UTM parameters or click ids into hidden form fieldsnothing: each lead's attribution records how the visitor arrivedAPI
carrying ?agent= or ?ref= to a form through a hidden field or localStoragePUT /v1/site { "landingParams": ["agent"] }API
an island caching the landing agent and writing it into every consumer formnothing — a roster page claims the visit; data-session-agent="false" keeps a form outauthoring
one notification address for the whole site and forwarding by hand, or a separate form per person so the recipient can be hard-codedPUT /v1/site { leadRouting } — the recipient is resolved at submission time from a submitted field, with an inbox or round-robin fallbackAPI
a submit handler, a thank-you page, or a ?submitted=1 check to confirm a submitnothing — forms post without reloading and swap themselves for a confirmation; validation errors render in place. data-success-message changes the wordingauthoring
a fetch to a Proof product's API from site.js to show its reply after a sign-updata-response-fields on the form, filled into data-mk-response-field slots on successAPI
a JS island that builds a query string and opens a booking/ticketing enginedata-action on <mk-component name="form"> — a real form to an allowlisted origin, so it works with no JSauthoring
an upload widget or "email us your résumé""type": "file" in a form schema — private, linked with the lead, kept one yearauthoring
a hardcoded copy of the field types a form acceptsGET /v1/manifest → forms.fieldTypes, derived from the renderer's own constantAPI
hard-coding a list of events or offers the client then cannot change or expire<mk-component name="collection"> over rows in /v1/collections — each row has a visibility window, so a finished event stops rendering on its ownauthoring
hand-authoring a second grid because one collection needs to appear as two different subsetsdata-where="<field>=<value>" on <mk-component name="collection"> — the grouping lives on the row, so both grids stay client-editableauthoring
letting a collection card fetch a picture sized for the whole viewport, or capping the row value and softening the detail pagedata-image-width on <mk-component name="collection"> — the card box in px; the row value's ?w= stays free for the detail pageauthoring
a JS island that turns a collection row's image field into an <img>, or hard-coding card art into the pagea collection field of type image or link — real <img> / <a> in the served HTML, with the platform's own width/height/srcset treatmentauthoring
building one page per event/offer, and deleting them as they passone page at /<base>/:slug with <mk-component name="collection-detail"> — the router binds the row, and its URL enters and leaves the sitemap on its ownauthoring
a page at /llms.txt (or /robots.txt), or a hand-maintained list of the site's pages for AI agentsnothing — /robots.txt, /sitemap.xml, /feed.xml and /llms.txt are served for you; improve /llms.txt by writing page meta.descriptions, starting with the home page'sauthoring
integrating an MLS feed (or pasting sample listings)listings + listing-detail components, server-rendered with the CREA obligations attached to the rows that owe themauthoring
hand-writing a card per new-construction lot, or giving a builder's lot the MLS® number and REALTOR.ca link it does not havethe same listings / listing-detail components over Evrylist — the CREA duties follow the row, and an unpriced lot renders price-on-request rather than $0authoring
hand-writing one agent card per person for a meet-the-team page, which then goes stale the day somebody joins or leaves<mk-component name="agent-roster"> over the DDF Member feed — no email or bio for anyone; everything else is per-member, so check what their feed returnsauthoring
standing up a separate site for an agent so their page can show only their listingsdata-agent-key on the listings component — the platform pairs it with the site's own office, so a key that is wrong or has moved brokerage renders empty rather than someone else's inventoryauthoring
pasting a seller-lead widget, or building the market panel for a 'what is my home worth' page from Places JS and a listings API<mk-component name="seller-comparables"> bound to a signed ?mk_addr= token from /__data/places/geocode — server-rendered, and it is NOT a valuationauthoring
writing stage + thumbnail-rail JS for a listing detail page, or re-arranging the lightbox's markup to build onedata-gallery="stage" on listing-detail — thumb clicks drive an unstyled stage, and the stage click opens the lightbox at that photoauthoring
copying a mortgage or land-transfer calculatorwindow.mk.mortgage() / window.mk.landTransferTax() — put data-mk-finance on any element of the page first, or the bundle never loads and window.mk carries only bindauthoring
writing a neighborhood page from the model's impression of the town, or asking the client for the local colourGET /v1/areas?name=… for the boundary id, then <mk-component name="neighborhood-stats"> — server-rendered per-area demographics and market data; a figure the vendor lacks is omitted, never printed as zeroauthoring
hard-coding one neighborhood into a listing detail template, so every property claims the same onedata-boundary="listing" on neighborhood-stats / community-map / market-trends — resolved from the listing's own coordinates by containment, falling back from neighborhood to cityauthoring
embedding a third-party map widget or an iframe just to show a neighborhood outline<mk-component name="community-map"> — a same-origin image of the boundary; needs no CSP change and the provider key stays server-sideauthoring
charting market figures a client emailed you, or loading a charting library from a CDN<mk-component name="market-trends"> — monthly closings and median sale price; renders not-covered rather than drawing a market the provider only partly hasauthoring
typing a community's average price and listing count into the page, or averaging a grid in JS<mk-component name="listing-stats"> — averages over the active feed, refreshed daily, with an inline SVG property-type donutauthoring
guessing which neighborhoods an agent should farm, or asking the model which area is 'up and coming'GET /v1/areas/rank?lat=&lng= — inventory, price, velocity and turnover per area, with the raw numbers and the method beside every score; relative to the result set, so it ranks rather than recommendsauthoring
writing carousel / tabs / scroll-reveal / lightbox / dropdown JSthe mk-* hook classes — behaviour only, you style itauthoring
writing a scroll-snap slider — arrow stepping, end-disabled arrows, a counter — or forcing mk-carousel to show four tiles at once.mk-track — your own overflow-x row; [data-mk-prev]/[data-mk-next] step by item width, the ends disable themselves, and swipe is native scrollingauthoring
writing thumb-click or dot-click JS to move a carousel, or settling for a thumb rail that opens the lightbox[data-mk-goto="n"] on a control inside .mk-carousel — 1-based, alongside [data-mk-prev]/[data-mk-next]authoring
a <noscript> fallback so scroll-reveal sections aren't blank without JSnothing — .mk-reveal ships revealed and the runtime un-reveals below the fold, so a failed script leaves content readable. Do write the .is-in-view ruleauthoring
an iframe with your own Google Maps key<mk-component name="map"> (the frame URL is signed for you)authoring
plotting listings on a map yourself, or re-querying the feed for coordinates<mk-component name="listings-map"> beside a listings grid — it re-queries the viewport as the visitor pans, so a "search this area" button is one you must not buildauthoring
asking a human for a brokerage's or agent's DDF key, or pasting one you were givenask Moseik with the agent's or brokerage's name — the keys are looked up from it; people give the board-scoped MlsId, which is not globally uniqueauthoring
asking a client for their Google Place ID, or lifting one out of a Maps URLask Moseik with the business name and town — a CID and the 0x…:0x… hex pair are NOT Place IDs, and both look plausible enough to pasteauthoring
loading the Google Places JS library, or putting a Maps key in page sourceGET /__data/places/autocomplete and /geocode (by placeId, with the autocomplete session) — same-origin, key stays server-side; connect-src 'self' means a third-party fetch fails to load, not just breaks a ruleauthoring
a Google reviews widget, an embed, or any script that fetches reviews in the browser<mk-component name="google-reviews"> — Place Details fetched SERVER-side, so the words are in the HTML a crawler reads; a vendor script cannot run under the page CSPauthoring
hard-coding a Place ID or maps URL to build a "see all our reviews" buttondata-mk-maps-url on the rendered .mk-reviews block, or reviews.mapsUrls on GET /v1/site — resolved from config, so it cannot driftauthoring
typing the options for a listing filter, or tallying them by hand from a page of resultsGET /__data/listings/facets — the values the feed actually holds, scoped to this site; an invented value returns an empty grid with no errorauthoring
filtering listings by price/beds/baths/size in the browser, or hiding cards with CSSdata-min-* / data-max-* on the listings component, and the same names on /__data/listings — one upstream query behind both the indexed grid and the visitor'sauthoring
sorting the rows you were handed, or a sort control that reorders one pagedata-sort="price-asc" / ?sort= — ordered across the WHOLE set, so page 2 follows page 1; a price sort returns sale listings onlyauthoring
a House / Condo / Townhouse dropdown built from data-property-type valuesdata-structure-type / ?structureType= — the feed files all three under one subtype, so dwelling type is a different field; take its values from facetsauthoring
a town or neighbourhood page whose filter quietly returns four other places, or a tile shipped as dead text because the search cannot be writtendata-match="exact" — the feed token-ORs, so "Fort Frances" otherwise matches every Fort and every Frances, and a board-prefixed neighbourhood goes wider stillauthoring
an island that reads location.search to make a listings search shareabledata-url-filters="all" on listings — the query string reaches the SERVER-rendered grid, which an island cannot do; ?page=N becomes real and the variant is noindexauthoring
an island that appends /__data/listings pages onto a grid as the visitor scrollsdata-paging="auto" (or "more") on listings — appends, and keeps the crawlable ?page=N link a scroll sentinel would deleteauthoring
hand-writing the filter form's JS — URL sync, back button, facets, 502-vs-empty<form class="mk-listings-filters" method="get"> with control names = filter names; a plain GET form that works with JS off, enhanced by the runtimeauthoring
pasting a <script type="application/ld+json"> blockPATCH /v1/pages/:id { meta: { schema } } — WebSite/Organization/LocalBusiness/BreadcrumbList already ship on every pageauthoring
writing RealEstateListing schema for a property pagenothing — listing detail routes emit it automatically from the feedauthoring
fetch() to an external API from your islandGET /__data/<provider> — the only route out under the CSPauthoring
guessing whether the CSP blocks what you are about to buildGET /v1/site → csp.effective — this site’s actual header, generated by the code that serves itAPI
probing endpoints to learn whether a feature is on, or filling a gap by handGET /v1/site → capabilities[] — every capability, on or off, with a reason code and who can turn it onAPI
posting form data to a CRM from the page, embedding a HubSpot form/script, or hand-rolling a webhook because the client's CRM is not HubSpotnothing in the page — build the form normally, then PUT /v1/lead-webhook { url, secret } (allowlist the origin first), or PUT /v1/crm-credential { token } for HubSpotAPI
assuming every form on a site has to send its leads to the same place, or routing them yourself on the receiving endPUT /v1/lead-webhooks/<name> { url, secret } per destination, then data-lead-destination="quotes" on the form. A form naming none still goes to all of themAPI
rebuilding a failed lead by hand and POSTing it at the CRM yourself, or telling the client an enquiry is unrecoverablePOST /v1/leads/<id>/redeliver — re-attempts the destinations that failed, idempotently. { destination } for one, { force: true } to override a terminal failure you have fixedAPI
pointing every agent on a brokerage site at one shared inbox, or resolving the agent yourself on the receiving endPUT /v1/lead-webhooks/<name> { provider: "engage", agentFromRouting: "email" } — no key to obtain; the lead reaches the routed agent's own MoxiWorks dashboardAPI
embedding Mailchimp's own signup form or script, or collecting newsletter signups for somebody to paste in by hand laterPUT /v1/lead-webhooks/<name> { provider: "mailchimp", apiKey, audienceId }, then route only the signup form to it — added as pending (double opt-in) unless the destination is set to subscribe directlyAPI
posting an event enquiry to Tripleseat from the page, or writing a receiver whose only job is to re-shape our lead into theirsPUT /v1/lead-webhooks/<name> { provider: 'tripleseat', apiKey } — the venue's lead-form public key, no url — then data-lead-destination on the enquiry form. Fields outside Tripleseat's twelve fold into additional_informationAPI
posting to a sheets API from the page, embedding a Google Form, or writing a receiver whose only job is to append a rowPUT /v1/lead-webhooks/<name> { provider: 'sheet', sheetId, tabName } — no credential; the call verifies the sheet is reachableAPI
telling a client that connecting their HubSpot needs a Moseik staff member, or waiting on onePUT /v1/crm-credential { token } with their Private App token — contacts-only scopes, and the client confirms the HubSpot account id it reports backAPI
publishing and hoping the client likes it, or describing a change in wordshold: true on POST /v1/pages (or ?hold=1 on a payload write), then POST /v1/pages/:id/versions/:vid/preview-link for a link they can open with no loginAPI
leaving an inherited old URL 404ing, or making a page just to hold a redirectPOST /v1/redirects { from, to } — to is a page path (router-resolved, so a collection row counts), a #fragment, a /media/<id> asset, or an allowlisted https:// URL; from may end /* to cover a whole subtree; refuses ones that shadow something live or point at a 404API
hand-maintaining a blog index, or modelling posts as collection rows and losing their metameta.publishedAt on each post page + <mk-component name="page-list" data-under="/blog/">; /feed.xml follows automaticallyauthoring
living with the generic not-found page, or building a real page at /404PUT /v1/notfound { html, css? } — rendered with your chrome, still 404 + noindexAPI
hotlinking images, resizing by hand, or uploading a small copy for a thumbnailPOST /v1/media/import → mirrored first-party; every request resized (1920px cap) + AVIF/WebP on serve, CSS url() included. ?w= overrides for small boxesAPI
a Google Fonts <link>vendored theme families, or POST /v1/fonts for a licensed woff2API
hard-coding colours and sizes on every pagetheme tokens → var(--color-primary)authoring
copy-pasting a header into forty pageschrome, partials, the site stylesheetauthoring
pasting a gtag or Meta pixel snippet, or building a cookie bannerPUT /v1/site { "analytics": { "ga4MeasurementId": "G-…", "metaPixelId": "…" } } — the platform loads both AND shows one consent bannerAPI
assuming the phone number and address on your contact page are what search engines readPUT /v1/site { "contact": { "phone", "email", "hours", "address" } } — the only source for the site's LocalBusiness data; fields merge, and it re-renders the siteAPI
typing the business's phone number and hours into every page that shows them<mk-component name="site-field" data-field="phone"> — prints one field of the contact record, so one PUT /v1/site { contact } corrects every page at onceauthoring
guessing what a page looks likeGET /v1/pages/:id/screenshot — a real browser under the real CSPAPI
writing a contrast checker, or driving four viewports yourself to find overflowGET /v1/pages/:id/verify — real browser: WCAG contrast, alt/label/heading a11y, mobile overflow, wrong image ratios, [hidden] that still has a boxAPI
submitting a form in a browser by hand, then deleting your test leadsPOST /v1/forms/:formKey/test — the real submit handler, reporting the stage it reached; the row it stores is filtered out of GET /v1/leadsAPI
assuming a form delivers because it rendersGET /v1/forms — which forms the handler will actually accept, and whether notifyEmail is setAPI
a JS helper so your own screenshots aren't blank below the fold?nolazy=1 / ?nomotion=1 on any page URL — handled at the serve layer, so it works on any site, including one you did not buildauthoring
re-authoring content you overwrote, from the copy that overwrote itPOST /v1/pages/:id/revert { toVersionId } — same for the stylesheet, script, chrome and partialsAPI
deleting a page, or leaving a dead one publishedPOST /v1/pages/:id/archive → 301 to a successor or 410 Gone; the URL keeps answering, the sitemap updates, the client gets a restore linkAPI
building a site locally, then asking your human where to host itPOST /v1/drafts → build it, then hand them the claimUrl (free 180 days)getting started
discovering the page cap or a frozen page by being refused one write at a timeGET /v1/site → plan.pages.remaining + plan.upgrades[], and frozen on each GET /v1/pages row — plan.restricted: false means nothing is limited, never "unknown"API
assuming a vendor origin will work under the CSPGET /v1/allowlist — check before you build; POST proposes one (reaches connect-src/form-action only, never script-src)API
a <link rel=icon> in your HTML (the platform owns the <head>)PUT /v1/favicon { assetId }API
asking your human to rename the site's moseik.app addressPUT /v1/site/address { "label" }API
letting a build collide with another agent editing the same sitePOST /v1/lease first, DELETE /v1/lease when done — a 409 names who holds it, and the acquisition response says what other credentials wrote since you were last here (sinceYourLastWrite) and names the assets they touched (changed[]) so you re-read those before editing. It lasts 300s; writing extends it, GET /v1/lease says whether you still hold it (youHoldIt, expiresInSeconds), and a write without it is 409 lease.required — a different code from a version conflict, so branch on the code for the remedy. The status tells you whether a retry can work at all: 409 is concurrency (do something, retry), 422 is the content guards (merge or acknowledge; no retry clears it)API
spending a write to find out if it passesPUT /v1/pages/:id/payload?dryRun=1API
one round trip per pageGET /v1/pages?include=payload, POST /v1/pages/batchAPI
reading a product's own API to learn what fields the form it asked for needs, or guessing themGET /v1/integration-requests/<id>/fields — mapped to Moseik field types, identity fields folded in, and a refusal naming any question this platform cannot renderAPI

<!-- END GENERATED: capability-table -->

GET /v1/manifest is the machine-readable form of this table (its capabilities array), plus the live budgets and the current component list. If you read one thing programmatically before building, read that.

Start here

For AI assistants

If you were handed an existing site's context and asked to make changes:

  1. Read Authoring pages and the API reference.
  2. Record docsVersion and manifestVersion, and check them at the start of every later run. GET /docs/versions.json gives the first plus a hash per doc; GET /v1/manifest gives the second plus a hash per section, and both honour If-None-Match — so "has anything changed?" is one conditional request that usually answers 304.

Your copy of this platform is a cache, and other agents and platform staff change the contract while you work. When a version moves, compare the per-item hashes to see which part moved, read the changelog, and re-read only that. A component description that changed under you is invisible any other way.

  1. Take the writer lease, then work through the content API only. You author per-site content (HTML, CSS, JS, theme tokens); you never edit platform code.
  2. Use the screenshot endpoint (GET /v1/pages/:id/screenshot) to see your own work — build one section at a time and compare against the target.

Every doc here is also available as raw Markdown for easy fetching — e.g. /docs/authoring.md, /docs/api.md.


Docs version af8c54ba7452 — machine-readable at /docs/versions.json. Returning after a gap? The changelog says what moved.