AI Website Builder
Let your own model build, improve, connect and publish a whole website. SoftSolz stores, guards, previews and hosts it.
The AI Website Builder hosts static websites: pages, shared header and footer, stylesheets, scripts and uploaded images. In the dashboard a person builds one by chatting. Through the public API an external model does the building: an assistant connected through the hosted MCP server, or your own server holding an sk_* key, designs the pages and writes the files, and SoftSolz stores them, runs the content guard, keeps the design tokens and navigation intact, takes a snapshot before every change, previews, publishes, hosts and connects the other SoftSolz services. Base path:
https://app.softsolz.uk/api/v1/services/site-builderNo endpoint spends SoftSolz AI credits
Every call on this API is free of SoftSolz AI credits, without exception. Creating a site, creating one from a starter, writing files, checking quality, connecting a service and publishing are all done without a SoftSolz model. The design work is yours, with your own model and your own credits. Every change you make is recorded as made by your app or key: the open builder page reloads within a few seconds and shows an Updated by <app> note, and the chat pane records what changed.
Connecting Claude, ChatGPT, Gemini or Cursor takes one address and no code; see Connect an AI assistant. Everything below applies equally to a connected assistant (each endpoint is one tool) and to a server calling the REST API with a key.
The file model
A website is a flat set of text files plus uploaded assets. Keep to this layout and every later change stays small: a new page is one file, a navigation change is one patch to one partial.
| Path | Holds |
|---|---|
index.html, about.html, ... | One page per file at the root. A page path is its web address, so never put a page in a folder. index.html is the home page and can never be deleted. |
partials/header.html, partials/footer.html | Shared chrome, written once. Each is a fragment (no doctype, html, head or body) and is pulled into a page by an include comment on its own line. In a partial, write a home-page section link as plain #contact; it becomes index.html#contact on every other page. |
css/base.css | The main stylesheet. Its first block, between the design-system markers, is managed by SoftSolz; write your own rules below it and use the tokens. |
js/site.js, js/chat.js, ... | One file per concern, each loaded by its own script tag. The site must work with JavaScript off. |
config.js | window.SITE_CONFIG with public keys and slugs only. Connecting a service in API mode merges its values in here. |
assets/ | Uploaded images and PDFs, referenced by their relative path, for example assets/hero.jpg. |
<!doctype html><html lang="en-GB"><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Sourdough and pastries in Leeds | Northwind Bakery</title> <meta name="description" content="Bread baked at dawn, pastries all day, and a counter that knows your name."> <link rel="stylesheet" href="css/base.css"></head><body><!-- softsolz:include partials/header.html --><main> <section id="hero">...</section></main><!-- softsolz:include partials/footer.html --><script src="js/site.js"></script></body></html>sitemap.xml, robots.txt and llms.txt are generated from your pages on every publish, so never write them. Text files may use the extensions html, css, js, json, svg, txt, xml and webmanifest; uploads may be png, jpg, jpeg, webp, gif, ico, svg or pdf.
The design-system block
The top of css/base.css holds a token block between /* design-system:start */ and /* design-system:end */. It defines --color-primary, --color-secondary, --color-accent, --color-bg, --color-surface, --color-text, --color-muted, --font-display, --font-body, --radius, --space-1 and up, and --shadow. Set it with PUT /projects/{id}/design-system (a style name, hex colours, Google Fonts families and the brand) and SoftSolz rewrites the block; a full write of the stylesheet keeps the block too. Every rule you write should use the tokens, so a palette change is one call rather than an edit to every file.
Limits
- 60 files per website, 400 KB per text file, 5 MB per uploaded asset.
- Up to 40 operations per apply call and 40 find-and-replace edits per patch.
- JSON request bodies are capped at 1 MB, so send large uploads as multipart.
- Site notes (memory) hold up to 4000 characters.
GET /guide returns the same file model, limits and rules as JSON, plus the design and code rules the workspace expects, so a session can start from the live version rather than from this page.
The content guard
Every text file is checked before it is stored, on every path: your writes, the dashboard chat and starter templates alike. A published site can only ever reach the documented public widget endpoints with its own public key. A file is refused when it contains:
- a secret or server key (
sk_*,svai_*); only the publicpk_*key belongs in a website eval(),new Function()ordocument.write()document.cookie, or a read of the dashboard session from local storageinsertAdjacentHTML, orinnerHTMLassigned anything but an empty string; build nodes withcreateElementand set text withtextContent- any
/api/path outside the public widget endpoints of Forms, Newsletter, Blogs, Knowledge Hub, Customer Auth, Appointments and the hosted pay page
External hosts are limited to Google Fonts and a placeholder image service; icons are inline SVG, never an icon font or a CDN.
| Code | Meaning | Do |
|---|---|---|
422 unsafe_site_content | A file broke one of the rules above. The message names the file and the reason. | Fix the file and send the batch again. Nothing was written. |
422 index_required | The batch would remove index.html. | Overwrite the home page instead of deleting it. |
422 not_editable | A text write targeted an uploaded asset. | Upload a new file or delete the asset. |
409 STALE_VERSION | The site changed since the content_version you sent as expected_version. | Read the files again, rebuild your patches on the current text, then retry. |
409 site_busy | The dashboard chat is mid-change on this website. | Wait a few seconds and send the same call again. |
A batch is applied as a whole after a snapshot, so a refused file never leaves the site half-written. Patches are the exception: a find that does not match exactly once is reported in patch_failures while the rest of the batch goes through, so re-read that file and send a corrected patch or a full write.
Worked flow: build and publish a site
1. Create the website
language is a BCP 47 tag and defaults to the workspace locale. SoftSolz writes it as the html lang of every page it generates, so set it to what the visitors read.
curl -X POST https://app.softsolz.uk/api/v1/services/site-builder/projects \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: site-northwind-bakery" \ -d '{ "name": "Northwind Bakery", "description": "Artisan bakery in Leeds with online ordering.", "language": "en-GB" }'{ "id": 42, "name": "Northwind Bakery", "slug": "northwind-bakery", "site_address": "northwind-bakery", "description": "Artisan bakery in Leeds with online ordering.", "language": "en-GB", "status": "draft", "live_url": null, "published_at": null, "starter_id": null, "has_unpublished_changes": true, "content_version": "2026-09-17T09:30:00.000Z", "last_edited_via": "api", "last_edited_label": "Build server", "preview_url": "https://app.softsolz.uk/api/services/site-builder/preview/42/1789000000-abc/index.html", "memory": null, "design_system": null, "domains": [], "created_at": "2026-09-17T09:30:00.000Z", "updated_at": "2026-09-17T09:30:00.000Z"}A blank site starts with a minimal placeholder home page, a header and footer partial, the stylesheet and a script. 402 quota_exceeded means the plan has no room for another website and 409 name_taken means the name is already used.
2. Read the guide
curl https://app.softsolz.uk/api/v1/services/site-builder/guide \ -H "Authorization: Bearer sk_live_your_key"{ "workflow": [ "Read this guide once per session, then create a website ...", "Read the current files before changing anything ...", "Design the whole site yourself, then send the files with POST /projects/{id}/files/apply ..." ], "file_model": { "home_page": "index.html", "partials": { "folder": "partials/", "include": "softsolz:include partials/header.html" }, "stylesheet": "css/base.css", "config": "config.js at the root holds window.SITE_CONFIG with public keys and slugs only.", "generated_by_platform": ["sitemap.xml", "robots.txt", "llms.txt"], "allowed_extensions": ["html", "css", "js", "json", "svg", "txt", "xml", "webmanifest"] }, "rules": [ { "kind": "design", "title": "Visual design", "content": "It must look deliberately designed at first glance. ..." }, { "kind": "responsive", "title": "Works on every screen", "content": "..." }, { "kind": "seo", "title": "Search and sharing", "content": "..." } ], "content_guard": { "refused": ["a secret API key. Only the public pk_ key belongs in a website.", "..."], "allowed_api_prefixes": ["..."] }, "design_system": { "markers": { "start": "/* design-system:start */", "end": "/* design-system:end */" }, "tokens": ["--color-primary", "..."] }, "integrations": { "connectable": ["forms", "newsletter", "smart-chat", "blogs", "customer-auth", "payments", "appointments"], "multi": ["forms"] }, "limits": { "max_files": 60, "max_file_bytes": 409600, "max_asset_bytes": 5242880, "max_operations_per_apply": 40, "max_edits_per_patch": 40, "request_body_bytes": 1048576 }, "concurrency": { "expected_version": "...", "site_busy": "..." }}The rules are the design, section, motion, icon, search, responsive, image and code rules the workspace expects, including any custom rules its members wrote. A site that follows them looks like one the dashboard built.
3. Read before you write
curl https://app.softsolz.uk/api/v1/services/site-builder/projects/42/files \ -H "Authorization: Bearer sk_live_your_key"{ "files": [ { "path": "css/base.css", "bytes": 1840, "is_binary": false, "summary": "Design tokens and base styles.", "updated_at": "2026-09-17T09:30:00.000Z" }, { "path": "index.html", "bytes": 920, "is_binary": false, "summary": "Placeholder home page.", "updated_at": "2026-09-17T09:30:00.000Z" }, { "path": "js/site.js", "bytes": 310, "is_binary": false, "summary": null, "updated_at": "2026-09-17T09:30:00.000Z" }, { "path": "partials/footer.html", "bytes": 260, "is_binary": false, "summary": null, "updated_at": "2026-09-17T09:30:00.000Z" }, { "path": "partials/header.html", "bytes": 340, "is_binary": false, "summary": null, "updated_at": "2026-09-17T09:30:00.000Z" } ], "content_version": "2026-09-17T09:30:00.000Z"}GET /projects/{id}/file?path=css/base.css returns one file as stored, and GET /projects/{id}/rendered?path=about returns a page with its partials stitched in, the way a visitor receives it. Keep content_version for the next step.
4. Write the site in one batch
write creates or replaces a text file, patch changes an existing file with exact find and replace pairs (each find must match exactly once), and delete removes a file. Operations apply in order.
curl -X POST https://app.softsolz.uk/api/v1/services/site-builder/projects/42/files/apply \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: northwind-first-build" \ -d '{ "expected_version": "2026-09-17T09:30:00.000Z", "label": "Home, about and contact pages", "operations": [ { "op": "write", "path": "index.html", "content": "<!doctype html>\n<html lang=\"en-GB\">\n<head>...</head>\n<body>\n<!-- softsolz:include partials/header.html -->\n<main>...</main>\n<!-- softsolz:include partials/footer.html -->\n<script src=\"js/site.js\"></script>\n</body>\n</html>" }, { "op": "write", "path": "about.html", "content": "<!doctype html>..." }, { "op": "write", "path": "contact.html", "content": "<!doctype html>..." }, { "op": "patch", "path": "partials/header.html", "edits": [ { "find": "<a href=\"index.html\">Home</a>", "replace": "<a href=\"index.html\">Home</a>\n<a href=\"about.html\">About</a>\n<a href=\"contact.html\">Contact</a>" } ] }, { "op": "patch", "path": "css/base.css", "edits": [ { "find": "/* design-system:end */", "replace": "/* design-system:end */\n\n.hero { background: var(--color-primary); color: var(--color-bg); padding: var(--space-5) 0; }" } ] } ] }'{ "changed": ["index.html", "about.html", "contact.html", "partials/header.html", "css/base.css"], "deleted": [], "patch_failures": [], "snapshot_id": 17, "content_version": "2026-09-17T09:31:00.000Z", "issues": [ { "code": "missing_alt", "severity": "warning", "path": "about.html", "detail": "An image has no alt text." } ]}snapshot_id is the version taken before the change; POST /projects/{id}/snapshots/{snapshot_id}/restore undoes it. Send the new content_version as expected_version on your next batch.
5. Check quality
curl "https://app.softsolz.uk/api/v1/services/site-builder/projects/42/quality?mode=build" \ -H "Authorization: Bearer sk_live_your_key"{ "issues": [ { "code": "dangling_nav_link", "severity": "error", "path": "partials/header.html", "detail": "Nav links to menu.html but that page does not exist." } ], "errors": 1, "warnings": 0}The checks cover links, the viewport tag, the stylesheet, alt text and the search head of every page. mode=edit with paths= runs only the fast checks on the files you name. Fix every error before publishing.
6. Show the person a preview
curl "https://app.softsolz.uk/api/v1/services/site-builder/projects/42/preview?page=index.html" \ -H "Authorization: Bearer sk_live_your_key"{ "url": "https://app.softsolz.uk/api/services/site-builder/preview/42/1789000000-abc/index.html", "page": "index.html", "expires_at": "2026-09-17T15:31:00.000Z"}The link is signed and works for about six hours, so it is safe to hand to a person in a chat reply.
7. Publish and poll
curl -X POST https://app.softsolz.uk/api/v1/services/site-builder/projects/42/publish \ -H "Authorization: Bearer sk_live_your_key" \ -H "Idempotency-Key: northwind-publish-1"{ "deployment_id": 7, "task_id": "t0000000-0000-0000-0000-000000000001"}curl https://app.softsolz.uk/api/v1/services/site-builder/tasks/t0000000-0000-0000-0000-000000000001 \ -H "Authorization: Bearer sk_live_your_key"{ "id": "t0000000-0000-0000-0000-000000000001", "kind": "site-builder.deploy", "status": "completed", "progress": { "total": 1, "processed": 1, "failed": 0 }, "error": null, "created_at": "2026-09-17T09:32:00.000Z", "started_at": "2026-09-17T09:32:01.000Z", "completed_at": "2026-09-17T09:32:40.000Z"}Poll every few seconds until status is completed, failed or cancelled, then read GET /projects/{id} for live_url. If you registered a webhook, site-builder.site.published fires at the same moment. 409 publish_in_progress means one is already running, and 409 integration_in_progress means a service is still being placed. Publishing again after more edits updates the live site; until then visitors keep seeing the last published version.
8. Add a domain
Only after the first publish.
curl -X POST https://app.softsolz.uk/api/v1/services/site-builder/projects/42/domains \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{"domain":"www.northwindbakery.co.uk"}'{ "id": 3, "domain": "www.northwindbakery.co.uk", "status": "pending", "dns_records": [ { "type": "CNAME", "name": "www", "value": "cname.vercel-dns.com" } ], "last_checked_at": null, "verified_at": null, "created_at": "2026-09-17T09:35:00.000Z"}Give the person the dns_records to create at their registrar, then call POST /projects/{id}/domains/{domain_id}/verify. DNS takes a while to propagate, so retry later while status stays pending; site-builder.domain.verified fires when it goes through.
Starters
A starter is a complete, designed website. Creating a site from one costs nothing and gives the model a polished base to adapt rather than a blank page.
curl https://app.softsolz.uk/api/v1/services/site-builder/starters \ -H "Authorization: Bearer sk_live_your_key"{ "id": "restaurant", "name": "Restaurant", "category": "landing", "tagline": "Menu, bookings and a warm welcome.", "description": "A restaurant site with menu, gallery and booking.", "tags": ["food"], "pages": [{ "path": "index.html", "title": "Home" }], "features": ["Menu with sections", "Gallery", "Opening hours"], "services": ["forms", "blogs", "newsletter", "smart-chat"], "thumbnail_url": "/images/starters/restaurant.png", "file_count": 21}curl -X POST https://app.softsolz.uk/api/v1/services/site-builder/projects/from-starter \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{"starter_id":"restaurant","name":"Northwind Bakery","language":"en-GB"}'The services array names the SoftSolz services the starter has a designed slot for. Connecting one of those touches no page: SoftSolz provisions the resource, mints the key and fills the slot, and the section the designer built comes alive. Then read the files and adapt copy, images and brand with files/apply as usual. 404 starter_not_found means the id is unknown.
Connect services without a model
A site is static; live behaviour (a form that lands in an inbox, a chat that answers from a knowledge base, a blog, a newsletter signup, a payment button, bookings, sign-in) comes only from connecting SoftSolz services. Never build a fake backend. One call installs the service if needed, validates or creates the resource, mints the public key and records the connection.
curl -X POST https://app.softsolz.uk/api/v1/services/site-builder/projects/42/integrations \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "service_id": "forms", "mode": "embed", "resource": { "create_form": { "name": "Contact", "fields": [ { "id": "name", "type": "text", "label": "Name", "required": true }, { "id": "email", "type": "email", "label": "Email", "required": true }, { "id": "message", "type": "textarea", "label": "Message", "required": true } ] } } }'{ "integration": { "id": 5, "service_id": "forms", "resource_ref": { "form_slug": "contact" }, "mode": "embed", "status": "connected", "snippet": "<div data-softsolz-widget ...></div>", "pages": [], "anchor": "softsolz-forms", "templated": false, "multi": true, "created_at": "2026-09-17T09:40:00.000Z" }, "placed": false, "pages": [], "changed": [], "placement": { "mode": "embed", "snippet": "<div data-softsolz-widget ...></div>", "instruction": "Add this SoftSolz form to the website. ... Wrap it in a section or div carrying id=\"softsolz-forms\" ... Paste the snippet exactly as given, exactly once.", "suggested_pages": ["contact.html", "index.html"], "anchor_id": "softsolz-forms", "marker": "data-form-slug=\"contact\"" }}Two outcomes:
placed: true(the site has a designed slot for the service): nothing else to do.pageslists the pages that changed andchangedthe files SoftSolz wrote, typicallyconfig.jsand, for Blogs, a reader page.placed: false: you place it withfiles/apply. Inembedmode pasteplacement.snippetexactly once, unmodified, inside a section carryingid="<anchor_id>"on one of thesuggested_pages, with a heading and context designed like every other section. Inapimode mergeplacement.config.valuesintowindow.SITE_CONFIGinconfig.js(never drop another service's keys) and build the section in the site style perplacement.instruction, reading everything from the config.markeris the text that must appear on a page once it is placed; the integrations list reports the pages it was found on.
mode defaults to embed, the ready-made hosted widget. api, a custom section against the public widget endpoints with the pk_* key from config.js, is available for forms, blogs, newsletter, chat and payments (400 api_mode_unsupported otherwise). Sign-in is widget-only: never build a custom login form. Forms can be connected more than once; every other service once (409 already_connected). The resource names what to connect: { form_slug } or { create_form } for forms, { card_slug } for newsletter, { booking_slug } for appointments, { payment_link_id } for payments, { kb_collection_ids } for chat, { view: "auth" | "profile" } for Customer Auth and {} for blogs. Only a published form or card can be connected.
Forms the site already has
GET /projects/{id}/site-forms parses every form designed into the pages and returns each one as a candidate: heading, page, inputs with their types and required flags. Pass one back as create_form and the SoftSolz form is published with field ids equal to the HTML name attributes, so the designed markup posts straight through with no change.
Customer Auth needs somewhere for sign-in to land. Place the widget, then read GET /projects/{id}/integrations: that read finds the pages carrying the sign-in widget, picks the site's own account or dashboard page as the destination, or writes a signed-in page in the site's own header and footer, and wires the widget to it. Re-read the file list before your next batch, because that write is a change like any other.
DELETE /projects/{id}/integrations/{integration_id} retires the connection. When removed is true SoftSolz took it out of the pages itself (a designed slot). When it is false, follow cleanup: delete the snippet or the config_key entry and any section or nav link that existed only for it. A connect while the dashboard is mid-change answers 409 site_busy; a workspace with no active plan, or one missing an email sender the service needs, answers 402.
Assets
Upload images and PDFs, then reference them by path. Multipart is the normal route; JSON with base64 content works for small files and is capped at 1 MB for the whole body. Up to 12 files per call, 5 MB each.
curl -X POST https://app.softsolz.uk/api/v1/services/site-builder/projects/42/assets \ -H "Authorization: Bearer sk_live_your_key" \ -F "files=@hero.jpg" \ -F "files=@logo.svg"curl -X POST https://app.softsolz.uk/api/v1/services/site-builder/projects/42/assets \ -H "Authorization: Bearer sk_live_your_key" \ -H "Content-Type: application/json" \ -d '{"files":[{"name":"logo.svg","content_base64":"PHN2ZyB4bWxucz0i..."}]}'{ "uploads": [ { "name": "hero.jpg", "path": "assets/hero.jpg", "size": 182000 }, { "name": "logo.svg", "path": "assets/logo.svg", "size": 2140 } ]}A file that fails comes back as { name, error } without stopping the others. Give every image explicit CSS bounds; an uploaded photo never goes on a page at its natural size.
Notes, snapshots and the dashboard
PUT /projects/{id}/memorykeeps what you learned about the business (palette, fonts, owner, decisions) with the site, so the dashboard chat and later sessions stay consistent.POST /projects/{id}/snapshotssaves a named version before a large change;GET /projects/{id}/snapshotslists the last 20, newest first, and a restore snapshots the current state first so it can always be undone.- Every write is attributed. The project carries
last_edited_via(dashboard,chat,apiorconnected_app) andlast_edited_label, the builder page reloads within a few seconds with an Updated by note, the chat pane records the batch, and the version history shows yourlabel. PATCH /projects/{id}renames the site or changes its description or language, guarded byexpected_updated_at. Changing the language does not rewrite the pages; update thehtml langattributes and the copy withfiles/apply.
Scopes
| Scope | Allows |
|---|---|
service.site-builder.view | Read websites. List websites, read files, rendered pages, snapshots, quality checks, preview links, deployments, domains and integrations, plus the guide, the starters and the workspace rules. |
service.site-builder.create | Create websites. Create a blank website or one from a starter template. Never spends AI credits. |
service.site-builder.edit | Edit website files and settings. Write, patch and delete files, take and restore snapshots, upload assets, set the design system and notes, connect and disconnect services. Never spends AI credits. |
service.site-builder.publish | Publish websites and manage domains. Start a publish, add, verify and remove custom domains. |
service.site-builder.delete | Delete websites. Permanently delete a website with its files, deployments and domains. |
A connected assistant only ever holds the scopes the member ticked on the consent screen, and a member can only grant scopes their own role already holds. A website in another workspace answers 404.
Events
| Event | When it fires |
|---|---|
site-builder.files.changed | The files of a website changed, from any source: the dashboard, the chat, an API key or a connected app. |
site-builder.site.published | A publish finished and the site is live at its address. |
site-builder.site.publish_failed | A publish could not be completed. |
site-builder.domain.verified | A custom domain passed its DNS check and now serves the site. |
site-builder.integration.connected | A SoftSolz service was connected to a website. |
site-builder.integration.removed | A SoftSolz service was disconnected from a website. |
site-builder.task.started | A background task, such as a publish, started. |
site-builder.task.completed | A background task finished. |
site-builder.task.failed | A background task could not be completed. |
site-builder.task.cancelled | A background task was cancelled. |
{ "project_id": 42, "changed": ["index.html", "about.html", "partials/header.html"], "deleted": [], "snapshot_id": 17, "content_version": "2026-09-17T09:31:00.000Z", "via": "connected_app", "label": "Claude"}changed and deleted list the paths, snapshot_id is the version taken before the change, content_version is the version after it, via is one of dashboard, chat, api or connected_app, and label is the app or key behind it. A build server can use it to know when the model has moved on without polling. Every delivery is signed; follow Webhooks for the signature check before you trust a payload.
Sandbox
An sk_test_* key, or a connected app that chose the Sandbox workspace, acts on the sandbox twin: its own websites, never the live ones. Everything above works there, which makes it the right place to develop a prompt or an integration, with two differences. Publishing is simulated: the deployment finishes with status: "simulated" and answers with a preview link rather than a public address. And custom domains are unavailable (400 domains_unavailable).
Next
Every endpoint, parameter, response shape and error code is generated into the API Reference, with samples in cURL, Node, Python and the SDKs. Connect an assistant in a minute with Connect an AI assistant, or read the product walkthrough on docs.softsolz.uk.