# Working in Hubbase as an AI agent

Hubbase (https://hubbase.app) is one admin for many websites and online stores: pages and a visual builder, products,
blog articles, SEO, marketing, social media, orders, payments, invoices, customers/CRM, dealers, live chat,
abandoned carts, email and market research. A workspace owner gave you an **agent key** with specific
permissions. This guide explains how to connect, what you may do and how to do common jobs well.

## 1. Connect

**REST (any agent that can make HTTP requests — Grok, ChatGPT actions, scripts):**
- Base URL: `https://hubbase.app`
- Header on every request: `Authorization: Bearer hbk_…` (your agent key)
- Bodies are JSON: `Content-Type: application/json`
- Start with `GET /api/agent/whoami` — it returns your permissions, the sites you may touch, per-site limits and
  the owner's notes. Re-read it whenever a request is refused.

**MCP (Claude, ChatGPT connectors, Gemini CLI, Cursor, any MCP client):**
- Server URL: `https://hubbase.app/mcp` with header `Authorization: Bearer hbk_…`
- Or, for clients that can't set headers (e.g. claude.ai custom connectors): `https://hubbase.app/mcp/hbk_…` (the key in the path — treat the URL as a secret)
- Tools: `hubbase_guide`, `hubbase_whoami`, `hubbase_list_sites`, `hubbase_api` (call any endpoint below),
  `hubbase_get_build`, `hubbase_save_build`, `hubbase_write_article`, `hubbase_seo_report`.

## 2. Permissions

Each area is **off**, **read** (GET only) or **write** (create/change). Three extra switches guard risky actions:
- **Can publish live** — publishing a site, going live, publishing an article, approving queue items that go live.
  Without it, save drafts and tell the person what is ready to publish.
- **Can move money** — refunds, captures, voids, charges, recording payments, affiliate payouts.
- **Can send emails & messages** — anything that emails or messages a customer, dealer or affiliate
  (order emails, invoice send/remind, cart reminders, waitlist notices, chat and social replies).

Sites can switch agents off or cap an area to read-only (Sites → site → AI agents). Some things are **never**
available to agents: API keys & secrets, team/invites, user accounts, platform admin, merchant/bank applications,
payment-gateway credentials, the email provider and its credentials, workspace payment/webhook/notification settings, deleting sites and managing agent keys. When something is refused you get HTTP 403
with a plain-English reason — tell the person exactly what switch they would need to change; don't try another route.

Everything you change is recorded in the workspace's agent activity log (who, what, when, result).

## 3. Rules of the road

1. **Read before you write.** Fetch the current record, change only what you were asked to, send it back.
   `PUT /api/sites/:id/build` replaces the whole draft — always GET it first and keep every page/block you didn't mean to change.
2. **Drafts first.** Builder edits (`PUT …/build`) are drafts. Articles can be saved with `status: "draft"`.
   Only publish when the person asked you to and your key allows it.
3. **Money is in cents** in stored records (product `price`, order totals). Some write endpoints take dollars — the
   catalog below says which. Double-check amounts before any money action and state them back to the person.
4. **Never invent facts** — prices, stock, SKUs, specs, warranty terms, certifications or where products are made.
   Use the catalog data. Follow each site's **notes** from `whoami` (brand rules, compliance, tone).
   For Sunstone / Austin Outdoor Kitchens sites: never state a warranty length outside the /warranty page and never
   claim a country of origin ("designed/engineered in Texas" is fine).
5. **Write like a person.** No filler ("in today's fast-paced world", "elevate", "unleash", "delve"), no emoji walls,
   short sentences, concrete details from the product data. `POST /api/marketing/lint` checks copy for AI tells and
   compliance problems before you save it.
6. **Customers are real people.** Before emailing/messaging a customer, show the person the exact text unless they told
   you to send without review. Keep replies factual and polite; hand anything about refunds, legal or safety to a human.
7. **Be economical.** List endpoints return many rows — filter with query params (`site`, `q`, `status`, `days`).
   Don't loop thousands of writes; batch where an endpoint accepts arrays.
8. **Report back.** Finish with what you changed (ids, pages, links like `https://hubbase.app/s/<slug>/<path>?draft=1` for drafts)
   and anything that needs a person (publish, approve, missing permission).

## 4. Common jobs

**Write and publish a blog article**
1. `GET /api/agent/whoami` → pick the site id; read its notes.
2. `GET /api/sites/:id/build` (products/categories come with it) or `GET /api/products?site=:id` for facts to cite.
3. `POST /api/marketing/lint` with your draft text; fix what it flags.
4. `POST /api/sites/:id/posts` with `{ title, slug, excerpt, content (HTML), cover, tags, status: "draft" }`.
   Use `status: "published"` only with **Can publish live**. For several sites at once use `POST /api/posts/network`.
5. Link to real product/category URLs (`/products/<slug>`, `/collections/<slug>`).

**SEO pass on a site**
1. `GET /api/sites/:id/seo` → score and issues.
2. Fix product meta via `PATCH /api/products/:id` (`seo: { title ≤ 60 chars, description ≤ 155 chars }`), page SEO via the
   build (`pages[].seo`), categories via `PATCH /api/categories/:id`, and broken URLs via `PUT /api/sites/:id/redirects`
   (send the FULL list — it replaces all redirects).
3. `POST /api/sites/:id/seo/fix` drafts missing product meta into the approval queue for a person to approve.

**Build or edit a page** — see section 6 (build format). GET the build, add/modify pages or blocks, PUT it back,
then share the draft preview link. Publish only if allowed: `POST /api/sites/:id/publish`.

**Products** — `GET /api/products?site=:id`, `GET/PATCH /api/products/:id`, `POST /api/sites/:id/products` (one or
`{ products: [...] }`). Prices in cents. Keep existing images/options unless asked.

**Orders** — `GET /api/orders?site=:id&status=…`, `GET /api/orders/:id`. Status changes like ship/deliver may email the
customer (needs **send**). Refund/capture/void need **money** — confirm the amount with the person first.

**Abandoned carts** — `GET /api/carts` to review; `POST /api/carts/remind` sends reminders (needs **send**);
`PUT /api/sites/:id/cart-recovery` changes automatic reminder timing.

**Live chat & inbox** — `GET /api/chats`, `POST /api/chats/:id/suggest` (AI draft), `POST /api/chats/:id/reply` (needs
**send**). `GET /api/inbox` for forms and leads.

**Marketing & social** — `POST /api/sites/:id/marketing/generate` drafts channel-ready copy in the brand voice;
`…/marketing/save` puts it on the calendar; social comments via `GET /api/sites/:id/social/inbox` and replies via
`POST /api/sites/:id/social/reply` (needs **send**).

**Market research** — `POST /api/research/discover` finds prospects, `/enrich` adds details, `/convert` makes CRM contacts.

**Plugins** (Website area) — `GET /api/plugins?scope=browse` lists plugins this workspace may use; `POST /api/sites/:id/plugins/:pid`
adds one to a site. `POST /api/sites/:id/plugins/extract` packages a site's custom features into *private* draft plugins
(run it after rebuilding a site). `POST /api/plugins` with `{kind:"section", name, blocks:[…]}` saves builder sections as a plugin.
Plugins belong to their owner: never change a plugin's visibility, sharing list or price unless the person explicitly asked —
their proprietary work must stay private by default. Buying plugins and marketplace administration are for people only.

## 5. API reference

### Website builder & pages (`website`)
Pages, blocks, theme, site settings, templates, imports, publishing drafts.

- `GET /api/sites` — List sites in the workspace (that the member may see) with page/product/order counts.
  - returns: { sites: [{id, name, slug, domain, kind, status, grp, description, template_id, published_at, updated_at, created_at, admin_url, pages, products, unread, orders, favicon, logo}] }
- `POST /api/sites` — Create a site: kind "built" starts from a template (or copies fromSiteId); kind "connected" registers an external website (owner/admin).
  - send: name, slug, domain, kind (built|connected), templateId, fromSiteId, group, description, adminUrl, useNameAsLogo
  - returns: { id }
- `GET /api/sites/:id` — Get one site's record (without theme/settings; use /build for those).
  - returns: { site: {id, name, slug, domain, kind, status, grp, description, template_id, public_key, published_at, has_secret, logo, ...} }
- `PATCH /api/sites/:id` — Update site name, group, description, status label, admin link, domain or slug (does not publish content).
  - send: name, grp, description, status (draft|live|paused), admin_url, domain, slug
  - returns: { ok }
- `POST /api/sites/:id/duplicate` — Copy a builder site (pages, theme, settings, catalog) into a new draft site (owner/admin).
  - send: name
  - returns: { id }
- `GET /api/sites/:id/build` — Load the draft build (theme, settings, pages with blocks) plus active products, categories and published posts for rendering. See BUILD_FORMAT.
  - returns: { site, theme, settings, pages: [{id, path, title, sort, in_nav, seo, blocks, published, updated_at}], products, categories, posts }
- `PUT /api/sites/:id/build` — Replace the WHOLE draft build (theme, settings, all pages); pages missing from the body are deleted. Saves a draft only; nothing goes live. See BUILD_FORMAT.
  - send: theme, settings, pages: [{id?, path, title, in_nav, seo, blocks}]
  - returns: { ok, pages: [{id, path}], savedAt }
- `POST /api/sites/:id/publish` — Make the current draft live (optionally saving a build in the same call); fails with 409 for connected sites, which must use go-live. **[publishes live]**
  - send: optional full build: theme, settings, pages
  - returns: { ok, publishedAt }
- `POST /api/sites/:id/go-live` — Switch a connected site to its Hubbase build and publish it, so Hubbase serves the domain instead of the old host (owner/admin). **[publishes live]**
  - returns: { ok, publishedAt, already? }
- `POST /api/sites/:id/convert` — Turn a connected site into a builder site in draft (adds an empty home page if none); it is not published (owner/admin).
  - returns: { ok }
- `POST /api/sites/:id/template` — Save a builder site as a reusable template in the workspace.
  - send: name, description, category, withProducts (bool)
  - returns: { id }
- `GET /api/templates` — List built-in and workspace templates.
  - returns: { templates: [{id, name, category, description, cover, pages, theme, builtIn?, custom?, shared?}] }
- `GET /api/templates/:id` — Get a template's full data (theme, settings, pages, products).
  - returns: { template }
- `PATCH /api/templates/:id` — Rename or recategorize a workspace template (built-ins cannot be changed).
  - send: name, category, description
  - returns: { ok }
- `DELETE /api/templates/:id` — Delete a workspace template.
  - returns: { ok }
- `GET /api/looks` — Workspace theme templates (saved colors/fonts/style usable on every site).
  - returns: { looks: [{id, name, theme}] }
- `POST /api/looks` — Save a theme template for the workspace (409 if the name exists unless replace:true).
  - send: name, theme, replace
  - returns: { look }
- `PATCH /api/looks/:id` — Rename or update a theme template.
  - send: name, theme
  - returns: { look }
- `DELETE /api/looks/:id` — Delete a theme template.
  - returns: { ok }
- `GET /api/plugins` — Browse plugins (scope=browse: everything this workspace may see; scope=mine: the workspace's own plugins). Filters q, source (claude|community), category.
  - send: scope, q, source, category (query)
  - returns: { plugins: [{id, name, tagline, kind, category, source, visibility, pricing, status, entitled, mine}], bundles, categories, kinds }
- `GET /api/plugins/:id` — One plugin with its contents list (only if this workspace may see it).
  - returns: { plugin }
- `POST /api/plugins` — Create a private plugin. kind=section with blocks (builder block objects) or kind=app with html (a full HTML page served at /apps/<appName>).
  - send: kind, name, tagline, description, category, icon, blocks | html, appName
  - returns: { id }
- `PATCH /api/plugins/:id` — Edit your own plugin: text, category, visibility (private|users|network), sharedWith (emails), pricing {model: free|one_time|subscription, price in cents, interval, sharedFree}, status (draft|published), removeItems. Network publishing goes to review. Only change sharing or pricing when the person asked you to.
  - send: name, tagline, description, category, icon, visibility, sharedWith, pricing, status, removeItems
  - returns: { plugin }
- `POST /api/plugins/:id/duplicate` — Copy one of your plugins as a new private draft.
  - returns: { id }
- `POST /api/sites/:id/plugins/extract` — Package a site's custom features (app pages, distinctive sections, kits, look, data sets) into private draft plugins. Safe to re-run; it updates rather than duplicates.
  - send: ai (bool, default true)
  - returns: { created, updated, ids }
- `GET /api/sites/:id/plugins` — Plugins installed on a site, with their builder sections.
  - returns: { plugins: [{id, name, kind, installed, sections: [{name, blocks}]}] }
- `POST /api/sites/:id/plugins/:pid` — Add a plugin this workspace is entitled to (free, shared or owned) to a site. Settings/looks are merged into the draft; publish to apply.
  - returns: { ok, notes }
- `DELETE /api/sites/:id/plugins/:pid` — Remove a plugin from a site (its app pages go; placed sections stay).
  - returns: { ok }
- `POST /api/sites/:id/import` — Bulk upsert data into a site by kind (categories, products, posts, qa, contacts, deals, activities, dealers, redirects, build, settings, docs, media-origin); kind "build" replaces pages and with publish:true also publishes (owner/admin). **[publishes live]**
  - send: kind, rows: [...] (each with ext id), build (kind build), publish (bool, kind build), settings (kind settings), origins (kind media-origin), replace (kind qa)
  - returns: { ok, created, updated }
- `GET /api/sites/:id/bundled-import` — Check (or with full=1 download) a catalog bundle shipped with this install for the site; install owner only.
  - send: query: full
  - returns: { available, name, generated_at, counts } or the raw bundle JSON
- `GET /api/sites/:id/docs` — List the site's content documents (JSON data stored for blocks and site code).
  - returns: { docs: [{name, size, updated_at}] }
- `GET /api/sites/:id/docs/:name` — Read one content document.
  - returns: { value }
- `PUT /api/sites/:id/docs/:name` — Create or replace one content document; used live by blocks immediately (names starting app_ need owner/admin).
  - send: value (any JSON)
  - returns: { ok }
- `GET /api/sites/:id/media-index` — Every image a site owns for the media picker: product photos, category images, uploads/AI images, post covers.
  - returns: { products: [{id, name, sku, slug, active, images, cats}], categories: [...], uploads: [{url, name, ai, at, size}], posts: [{title, url}] }
- `POST /api/theme/from-url` — Read a public web page's CSS and suggest a Hubbase color palette (read-only).
  - send: url
  - returns: { palette: {primary, accent, bg, surface, text, muted, dark}, colors: [{hex, n, bg, text}], sheets, themeColor } or { error }
- `GET /api/sites/:id/dealer-portal` — Get the dealer portal settings (tabs, welcome, resources, training).
  - returns: { portal }
- `PUT /api/sites/:id/dealer-portal` — Save dealer portal settings; applies to the live portal immediately.
  - send: portal: {tabs: {key: {on, label}}, title, welcome, banner, accountManager, supportPhone, supportEmail, ytdTarget, training: {title, body, pdf, video}, resources: [{title, url, section, type, thumb}], requireTaxCert}
  - returns: { ok }
- `POST /api/import/detect` — Check an existing website before importing: platform (WordPress, Shopify, Wix …), page/post/product counts, colors.
  - send: { url }
  - returns: { platform, platformName, siteName, counts, theme: {palette}, notes }
- `POST /api/sites/:id/import/wordpress` — Import from a WordPress site's REST API one step at a time. Steps: site, pages, posts, categories, products (WooCommerce). Repeat with page+1 until done.
  - send: { url, step, page, user?, appPassword? }
  - returns: { created, updated, redirects?, done, next, total? }
- `POST /api/sites/:id/import/shopify` — Import from a Shopify store's public feed. Steps: collections, collection-products ({handle}), products ({links: {productId: [collectionExt]}}).
  - send: { url, step, page, handle?, links? }
  - returns: { created, updated, done, next }
- `POST /api/sites/:id/import/sitemap` — List an existing site's page addresses from its sitemap (or home page links).
  - send: { url, max? }
  - returns: { urls }
- `POST /api/sites/:id/import/crawl` — Import up to 12 pages by address (content only — menus and footers dropped) as editable pages.
  - send: { url, urls: [] }
  - returns: { created, updated, pages, failed }
- `POST /api/sites/:id/import/pages` — Import pages from HTML you already have.
  - send: { pages: [{ path, title, html, description?, main? }] }
  - returns: { created, updated }
- `POST /api/sites/:id/import/posts` — Import blog posts from HTML (old addresses redirect).
  - send: { posts: [{ slug, title, html, excerpt?, cover?, author?, tags?, date?, old? }] }
  - returns: { created, updated, redirects }

### Products & categories (`catalog`)
Products, categories, prices, Q&A, reviews, dealer pricing.

- `GET /api/brands` — Product network: brands this workspace can see, with its membership status (pending/approved) for each.
  - returns: { brands: [...] }
- `GET /api/brands/:id` — One brand: terms, top categories, sample products with retail/MAP (and dealer cost once approved).
  - returns: { brand, membership, sample }
- `GET /api/sites/:id/brands` — Brands a site carries (product sources): pricing rule, last sync, product counts.
  - returns: { sources }
- `POST /api/sites/:id/brands/:brandId` — Add an approved brand's catalog to a site, or change its pricing rule; syncs at once.
  - send: mode (retail|map|discount|markup), pct, group (bool)
  - returns: { created, updated, total }
- `POST /api/sites/:id/brands/:brandId/sync` — Sync a brand's products into the site now.
  - returns: { created, updated }
- `GET /api/sites/:id/brands/:brandId/products` — A brand's products on a site with retail, MAP, cost and the dealer's own price (cents).
  - returns: { products }
- `POST /api/sites/:id/brands/:brandId/price` — Set (or clear with null) the site's own price for one network product, in dollars; never below the brand's MAP.
  - send: productId, price
  - returns: { price }
- `GET /api/products` — List products across the workspace (all columns).
  - send: query: site
  - returns: { products: [...] }
- `GET /api/sites/:id/products` — List one site's products (all columns).
  - returns: { products: [...] }
- `POST /api/sites/:id/products` — Create one product, or up to 500 when products[] is sent; a new category named in category is created.
  - send: name (required), slug, sku, price (cents), compare_at (cents), description (HTML), short_description, images [url], category, categories [category ids], stock, featured, active, sort, options, specs [[k,v]], features [], documents [{title,url}], videos, reviews, tags, seo {title, description}, flags; or products: [...]
  - returns: { ids }
- `GET /api/products/:id` — Get one product with parsed JSON fields and its category ids.
  - returns: { product }
- `PATCH /api/products/:id` — Update a product; only fields sent change; the live store reflects it immediately.
  - send: same fields as create (prices in cents); categories replaces category links
  - returns: { ok }
- `DELETE /api/products/:id` — Delete a product.
  - returns: { ok }
- `GET /api/sites/:id/categories` — List a site's categories with product counts.
  - returns: { categories: [{id, parent_id, slug, name, description, image, banner, sort, active, seo, products}] }
- `POST /api/sites/:id/categories` — Create a category.
  - send: name (required), slug, parent_id, description, image, banner, sort, active, seo
  - returns: { id }
- `PATCH /api/categories/:id` — Update a category.
  - send: name, slug, parent_id, description, image, banner, sort, active, seo
  - returns: { ok }
- `DELETE /api/categories/:id` — Delete a category (children move up to its parent; product links removed).
  - returns: { ok }
- `POST /api/sites/:id/copy-catalog` — Copy all categories and products from this site to another site whose catalog is empty (owner/admin).
  - send: toSiteId
  - returns: { ok, products, categories }
- `GET /api/sites/:id/price-levels` — Get dealer price levels (multipliers) for a site.
  - returns: { levels: [{key, label, multiplier}], defaults }
- `PUT /api/sites/:id/price-levels` — Replace dealer price levels; applies live immediately (owner/admin).
  - send: levels: [{key, label, multiplier (0.05-1)}]
  - returns: { ok }
- `GET /api/sites/:id/dealer-store` — Dealer pricing view of a site's products (dealer price, shipping mode, MOQ, dealer-only flags).
  - returns: { priceBase, levels, products: [{id, ref, name, sku, price, compare_at, category, active, dealerShip, dealerSurcharge, dealerNote, dealerOnly, moq, dealerPrice, noDiscount}] }
- `PUT /api/sites/:id/dealer-store` — Set whether dealer prices are based on price or MAP.
  - send: priceBase (price|map)
  - returns: { ok }
- `POST /api/sites/:id/dealer-store/bulk` — Bulk-set dealer flags on selected products, a category, or all products.
  - send: ids [] | category | all (bool), patch: {dealerShip (included|ltl|surcharge), dealerSurcharge, dealerNote, dealerOnly, moq, dealerPrice (dollars or null), noDiscount}
  - returns: { ok, updated }
- `GET /api/qa` — List product questions and answers.
  - send: query: site, status (pending|published|hidden), q
  - returns: { items: [{id, site_id, product_id, question, answer, status, product_name, product_slug, ...}], counts }
- `PATCH /api/qa/:id` — Answer or moderate a product question; answering a pending question publishes it on the live product page.
  - send: answer, status (pending|published|hidden)
  - returns: { ok }
- `DELETE /api/qa/:id` — Delete a product question.
  - returns: { ok }
- `GET /api/reviews` — List product reviews for moderation.
  - send: query: site, status (pending|published|hidden)
  - returns: { reviews: [...], counts }
- `PATCH /api/reviews/:id` — Moderate or edit a review and set the public store reply (shown live when published).
  - send: status (pending|published|hidden), reply, title, body, rating (1-5)
  - returns: { ok }
- `DELETE /api/reviews/:id` — Delete a review.
  - returns: { ok }

### Blog & articles (`content`)
Write, edit and publish articles across sites.

- `GET /api/posts` — List blog articles across sites, grouped so one article shared to several sites appears once.
  - send: query: q
  - returns: { posts: [{key, id, site_id, group_id, slug, title, excerpt, cover, author, status, published_at, copies: [{id, site_id, status, slug}]}] }
- `POST /api/posts/network` — Create or update one article on several sites at once; status "published" makes it live on each.
  - send: site_ids ([ids] or "all"), id or group_id (to update), title, slug, excerpt, content (HTML), cover, author, tags [], status (draft|published), published_at, remove_others (bool)
  - returns: { group_id, posts: [{id, site_id}] }
- `GET /api/sites/:id/posts` — List one site's articles.
  - returns: { posts: [{id, slug, title, excerpt, cover, author, status, published_at, updated_at}] }
- `POST /api/sites/:id/posts` — Create an article on one site; status "published" makes it live immediately.
  - send: title (required), slug, excerpt, content (HTML), cover, author, tags [], status (draft|published), published_at
  - returns: { id }
- `GET /api/posts/:id` — Get one article with its copies on other sites.
  - returns: { post, copies: [{id, site_id, status}] }
- `PATCH /api/posts/:id` — Update an article; status "published" makes it live immediately.
  - send: title, slug, excerpt, content, cover, author, tags, status, published_at
  - returns: { ok }
- `DELETE /api/posts/:id` — Delete an article (all=1 deletes its copies on every site).
  - send: query: all
  - returns: { ok }

### SEO (`seo`)
SEO reports, meta fixes and redirects.

- `GET /api/sites/:id/seo` — SEO audit of a site: score and issues (missing titles, meta descriptions, thin copy, photos).
  - returns: { score, issues: [{level, area, what, link, target}], counts, aiReady, sitemap }
- `POST /api/sites/:id/seo/fix` — Run the AI SEO agent to draft missing product meta titles/descriptions into the approval queue (nothing goes live until approved).
  - returns: { ok, summary }
- `GET /api/sites/:id/redirects` — List the site's URL redirects.
  - returns: { redirects: [{from_path, to_path}] }
- `PUT /api/sites/:id/redirects` — Replace ALL redirects for the site (send the full list); applies live immediately (owner/admin).
  - send: redirects: [{from_path, to_path}]
  - returns: { ok, count }

### Marketing & automations (`marketing`)
Marketing studio, campaigns, calendar, automations and the approval queue.

- `GET /api/marketing/formats` — Channel formats and their rules, posting cadence, and banned words/phrases for copy.
  - returns: { formats, cadence, banned: {words, phrases} }
- `POST /api/marketing/lint` — Check marketing copy against channel rules and banned words.
  - send: text, format, brand
  - returns: { issues: [{type, msg}] }
- `GET /api/sites/:id/marketing/brand` — Get the site's brand voice profile.
  - returns: { brand }
- `PUT /api/sites/:id/marketing/brand` — Save the site's brand voice profile.
  - send: brand: {name, tagline, what, different, audiences, pillars, always, never, ctas, hashtags, tone, ...}
  - returns: { ok }
- `POST /api/sites/:id/marketing/brand-draft` — AI-draft a brand voice profile from the site's products and settings (not saved).
  - returns: { draft }
- `POST /api/sites/:id/marketing/generate` — AI-write marketing content for one source across chosen channels (not saved or posted).
  - send: formats [instagram|instagram_carousel|reel|facebook|linkedin|x|pinterest|gbp|youtube|short|email|google_ads|meta_ads|blog_brief], source: {type (product|category|post|topic), id, topic, link}, goal (awareness|consideration|conversion|community), notes
  - returns: { items: [{format, text, issues, ...}], source, aspect }
- `POST /api/sites/:id/marketing/fix` — AI-rewrite one piece of copy so it passes the checker (not saved).
  - send: format, text, ask
  - returns: { text, issues }
- `POST /api/sites/:id/marketing/plan` — AI-plan a content calendar for 1-8 weeks (not saved).
  - send: weeks, formats [], start (YYYY-MM-DD), theme
  - returns: { plan: [{date, format, goal, source, title, angle, theme}] }
- `POST /api/sites/:id/marketing/save` — Save content drafts to the approval queue (optionally dated for the calendar); nothing is posted.
  - send: items: [{format, title, text, date, link, image, goal, angle, theme, source, raw}]
  - returns: { saved }
- `GET /api/sites/:id/marketing/calendar` — Dated queue items (pending and approved) for a month.
  - send: query: month (YYYY-MM)
  - returns: { month, items: [...] }
- `GET /api/agents` — List AI content agents, recent runs, queue counts and agent kinds.
  - returns: { agents, runs, counts, kinds, aiReady }
- `POST /api/agents` — Create an AI agent for a site (blog, product_copy, seo, social, review_reply, lead_followup) that drafts into the approval queue.
  - send: kind, site_id, name, instructions, schedule (manual|daily|weekly)
  - returns: { id }
- `PATCH /api/agents/:id` — Update an agent's name, instructions, schedule, site or active flag.
  - send: name, instructions, schedule, active, site_id
  - returns: { ok }
- `DELETE /api/agents/:id` — Delete an agent.
  - returns: { ok }
- `POST /api/agents/:id/run` — Run an agent now; its drafts go to the approval queue (uses AI credits).
  - returns: { ok, summary }
- `GET /api/queue` — List approval-queue drafts.
  - send: query: status (pending|approved|rejected, default pending), site, kind
  - returns: { items: [{id, site_id, agent_id, kind, target, title, body, data, status, agent, created_at}] }
- `PATCH /api/queue/:id` — Edit a draft and/or approve or reject it; approving APPLIES it live (publishes a blog post, overwrites product copy/SEO, posts a review reply). **[publishes live]**
  - send: title, body, data, status (approved|rejected|pending)
  - returns: { ok, result }

### Social media (`social`)
Social accounts, posts, comments and replies.

- `GET /api/sites/:id/social` — Social networks for the site, which are connected, and the chosen Facebook Page.
  - returns: { networks: [{key, label, connected, ready, ...}], pages, page, redirects }
- `GET /api/sites/:id/social/connect-meta` — Browser-only: redirects to Facebook OAuth to connect a Page/Instagram (owner/admin); not usable by agents.
  - returns: 302 redirect
- `GET /api/sites/:id/social/connect-youtube` — Browser-only: redirects to Google OAuth to connect YouTube (owner/admin); not usable by agents.
  - returns: 302 redirect
- `GET /api/social/meta/callback` — Browser-only OAuth callback from Facebook; not for agents.
  - send: query: code, state
  - returns: 302 redirect
- `GET /api/social/youtube/callback` — Browser-only OAuth callback from Google; not for agents.
  - send: query: code, state
  - returns: 302 redirect
- `POST /api/sites/:id/social/disconnect` — Disconnect Facebook/Instagram or YouTube from the site (owner/admin).
  - send: network (youtube | anything else = meta)
  - returns: { ok }
- `POST /api/sites/:id/social/page` — Choose which connected Facebook Page the site uses (owner/admin).
  - send: id (page id)
  - returns: { ok }
- `GET /api/sites/:id/social/feed` — Recent posts on a connected network with engagement totals.
  - send: query: network (facebook|instagram|youtube), limit
  - returns: { posts, totals: {likes, comments, views, shares} }
- `GET /api/sites/:id/social/comments` — Comments on one social post.
  - send: query: network, post
  - returns: { comments }
- `GET /api/sites/:id/social/inbox` — Newest comments across all connected networks, marked answered or not.
  - returns: { comments: [{..., network, post, answered}], errors }
- `POST /api/sites/:id/social/reply` — Post a public reply to a social comment on Facebook, Instagram or YouTube. **[contacts customers]**
  - send: network, comment (comment id), text
  - returns: { ok, id }
- `POST /api/sites/:id/social/moderate` — Like, hide or unhide a social comment (publicly visible effect).
  - send: network, comment, do (like|hide|unhide)
  - returns: { ok }
- `POST /api/sites/:id/social/suggest` — AI-suggest replies to a social comment in the brand voice (not posted).
  - send: network, post, author, text
  - returns: { replies }

### Coupons & affiliates (`promotions`)
Coupons, affiliate program, referrals.

- `GET /api/coupons` — List coupons with order counts and total discount given.
  - returns: { coupons: [{id, site_id, code, kind, value, min_subtotal, starts_at, ends_at, max_uses, uses, dealers, active, notes, affiliate_id, orders, discounted}] }
- `POST /api/coupons` — Create a coupon; active coupons work at checkout immediately.
  - send: code, kind (percent|fixed|free_shipping), value (percent, or dollars for fixed), min_subtotal (dollars), site_id, starts_at, ends_at, max_uses, dealers (bool), active, notes
  - returns: { id }
- `PATCH /api/coupons/:id` — Update a coupon (send all fields; missing ones reset to defaults).
  - send: same as create
  - returns: { ok }
- `DELETE /api/coupons/:id` — Delete a coupon.
  - returns: { ok }
- `GET /api/affiliates` — Affiliates with stats, referrals, payouts, program settings and signup links.
  - send: query: status (referral status)
  - returns: { affiliates, referrals, payouts, program, signupUrls }
- `POST /api/affiliates` — Add an affiliate (optionally with a linked coupon); an active affiliate gets a welcome email unless welcome:false. **[contacts customers]**
  - send: name, email (required), code, site_id, phone, company, website, status (active|pending), commission_type (percent|flat), commission_rate, cookie_days, payout_method, payout_details, notes, coupon, coupon_percent, welcome
  - returns: { id, code }
- `PATCH /api/affiliates/:id` — Update an affiliate; activating one sends a welcome email unless welcome:false. **[contacts customers]**
  - send: name, email, phone, company, website, site_id, code, status (pending|active|paused|rejected), commission_type, commission_rate, cookie_days, payout_method, payout_details, notes, tier, coupon, coupon_percent, welcome
  - returns: { ok }
- `DELETE /api/affiliates/:id` — Delete an affiliate with no unpaid commissions (owner/admin).
  - returns: { ok }
- `POST /api/affiliates/:id/payout` — Record a payout of approved commissions (marks them paid) and email the affiliate unless notify:false (owner/admin). **[moves money]** **[contacts customers]**
  - send: referral_ids [], method, reference, notes, notify
  - returns: { ok, amount }
- `PUT /api/affiliates/program` — Save affiliate program settings (rates, cookie days, tiers, signup) (owner/admin).
  - send: defaultRate, type (percent|flat), cookieDays, autoApproveDays, signupOpen, autoActivate, terms, tiers: [{name, minRevenue, rate}]
  - returns: { ok }
- `POST /api/affiliates/tiers/run` — Re-tier active percent affiliates by revenue, raising their commission rate where earned (owner/admin).
  - returns: { ok, changed }
- `PATCH /api/referrals/:id` — Approve or reject a referral commission, optionally adjusting the amount.
  - send: status (pending|approved|rejected), commission (dollars)
  - returns: { ok }

### AI images & media (`ai_media`)
AI images/video, media library uploads.

- `GET /api/media` — List the workspace's uploaded files.
  - returns: { media: [{id, key, name, type, size, url, created_at}], enabled }
- `POST /api/media` — Upload a file (multipart form field "file"; images, MP4/WebM or PDF, max 10 MB).
  - send: multipart/form-data: file
  - returns: { id, url }
- `DELETE /api/media/:id` — Delete an uploaded file; pages still linking to it will show a broken image.
  - returns: { ok }
- `GET /api/ai/status` — Which AI image/video engines are configured and the available scenes.
  - send: query: site
  - returns: { ready, engines, video, scenes, storage }
- `POST /api/ai/image` — Generate 1-4 AI product photos from reference images into the media library (uses AI credits).
  - send: query: site; sources [urls] (required), n, aspect, scene (patio|pool|covered|hillcountry|modern|studio|studiogrey|custom), prompt, name, category, details, package (bool), angle, engine (gemini|grok)
  - returns: { images, prompt, engine }
- `POST /api/ai/spin-plan` — Compute the camera angles for a 180/360 degree spin set.
  - send: degrees, frames
  - returns: { angles }
- `POST /api/ai/video` — Start an AI tour video from one photo; poll GET /api/ai/video/:id for the result (uses AI credits).
  - send: query: site; source (required), move (orbit|push|walk|reveal), prompt, duration (4-15), resolution (480p|720p|1080p)
  - returns: { request_id }
- `GET /api/ai/video/:id` — Poll a video job; when done the video is saved to the media library.
  - send: query: site
  - returns: { status (working|done|failed), url?, progress?, error? }

### Orders & fulfillment (`orders`)
Orders, shipping, delivery, order emails.

- `GET /api/orders` — List up to 300 recent orders.
  - send: query: site, status, q (number/email/name)
  - returns: { orders: [{id, site_id, number, source, status, payment_status, payment_method, tracking_number, customer_name, email, total, currency, created_at, itemCount}] }
- `GET /api/orders/export.csv` — Download orders as CSV (money in dollars).
  - send: query: site
  - returns: text/csv
- `GET /api/orders/:id` — Get one order with items, address and data.
  - returns: { order }
- `PATCH /api/orders/:id` — Set order status, payment-status label or internal notes (no email, no money moved).
  - send: status (new|processing|shipped|completed|cancelled), payment_status (unpaid|authorized|paid|refunded|test), notes
  - returns: { ok }
- `GET /api/orders/:id/activity` — Order payments, sent emails, affiliate referral, linked invoice, captured/refunded/authorized totals and links.
  - returns: { payments, emails, referral, invoice, captured, refunded, authorized, links: {order, pay}, carriers, gateway }
- `GET /api/orders/:id/slip` — Printable packing slip (or invoice with kind=invoice) as HTML.
  - send: query: kind
  - returns: text/html
- `POST /api/orders/:id/ship` — Add tracking, mark shipped and email the customer a shipping notice unless notify:false. **[contacts customers]**
  - send: carrier, tracking_number, tracking_url, eta, notify
  - returns: { ok, email }
- `POST /api/orders/:id/tracking` — Update tracking details without marking shipped or emailing.
  - send: carrier, tracking_number, tracking_url, eta
  - returns: { ok, email }
- `POST /api/orders/:id/deliver` — Mark an order delivered/completed; emails the customer only if notify:true. **[contacts customers]**
  - send: notify
  - returns: { ok, email }
- `POST /api/orders/:id/cancel` — Cancel an order, restock items (unless restock:false) and reject its affiliate commission; does NOT refund.
  - send: restock
  - returns: { ok }
- `POST /api/orders/:id/refund` — Refund money on the card gateway (or record an offline refund), update the order and email the customer unless notify:false (owner/admin). **[moves money]** **[contacts customers]**
  - send: amount (dollars, default remaining), payment_id, reason, manual (bool), cancel (bool), restock (bool), notify
  - returns: { ok, refunded }
- `POST /api/orders/:id/void` — Void the latest gateway payment or authorization before settlement (owner/admin). **[moves money]** **[contacts customers]**
  - send: payment_id, reason
  - returns: { ok, refunded }
- `POST /api/orders/:id/capture` — Capture (charge) an authorized card payment and mark the order paid (owner/admin). **[moves money]**
  - send: payment_id, amount (dollars, default full authorization)
  - returns: { ok }
- `POST /api/orders/:id/record-payment` — Record an offline payment (check, cash, wire) against the order. **[moves money]**
  - send: amount (dollars, default order total), method, note
  - returns: { ok, paid }
- `POST /api/orders/:id/pay-link` — Get the order's secure payment link; with send:true emails it to the customer. **[contacts customers]**
  - send: send (bool)
  - returns: { ok, url, email }
- `POST /api/orders/:id/email` — Email the customer an order template now. **[contacts customers]**
  - send: template (order_confirmation|order_shipped|order_delivered|review_request|payment_receipt)
  - returns: { ok, email }
- `POST /api/orders/:id/invoice` — Start a draft invoice from an order (not sent).
  - returns: { id, number }

### Payments & billing (`billing`)
Payments, invoices, plans, subscriptions, costs, tax certificates.

- `GET /api/payments` — List card/manual payment transactions with totals and each site's gateway.
  - send: query: site, status, kind, q, days (default 30)
  - returns: { payments, totals: {sales, refunds, net, count, declined}, gateways }
- `GET /api/invoices` — List invoices with outstanding/overdue stats.
  - send: query: site, status (draft|sent|viewed|partial|paid|void|open), q
  - returns: { invoices: [{id, site_id, number, status, customer_name, company, email, total, paid, currency, due_date, sent_at, paid_at, created_at, overdue}], stats }
- `POST /api/invoices` — Create a draft invoice (not sent); also adds the customer to the CRM.
  - send: site_id (required), customer_name, company, email, phone, address, items [{name, sku, qty, price (dollars)}], discount (dollars), shipping (dollars), tax_rate (%), due_date, notes, terms, allow_partial, dealer_id, number
  - returns: { id, number }
- `GET /api/invoices/:id` — Get an invoice with its payments, email log and pay link.
  - returns: { invoice, payments, emails, url, gateway }
- `PATCH /api/invoices/:id` — Edit an unpaid, non-void invoice.
  - send: same fields as create
  - returns: { ok }
- `DELETE /api/invoices/:id` — Delete an invoice with no payments.
  - returns: { ok }
- `GET /api/invoices/:id/print` — Printable invoice as HTML.
  - returns: text/html
- `POST /api/invoices/:id/send` — Email the invoice with its pay link to the customer and mark it sent. **[contacts customers]**
  - returns: { ok, email, url }
- `POST /api/invoices/:id/remind` — Email the customer a payment reminder for the invoice. **[contacts customers]**
  - returns: { ok, email, url }
- `POST /api/invoices/:id/void` — Void an invoice that has no payments.
  - returns: { ok }
- `POST /api/invoices/:id/record-payment` — Record an offline payment on an invoice; when fully paid an order is created. **[moves money]**
  - send: amount (dollars, default balance), method, note
  - returns: { ok, paid, full }
- `POST /api/invoices/:id/duplicate` — Copy an invoice into a new draft.
  - returns: { id }
- `GET /api/plans` — Subscription plans (with subscriber counts and signup URLs), subscriptions and MRR.
  - send: query: status (subscription status)
  - returns: { plans, subscriptions, mrr, gatewayReady }
- `POST /api/plans` — Create a subscription plan customers can sign up for.
  - send: site_id, name, amount (dollars), interval (week|month|quarter|year), interval_count, trial_days, setup_fee (dollars), cycles, product_id, description, public
  - returns: { id }
- `PATCH /api/plans/:id` — Update a plan (new price applies to new sign-ups only).
  - send: name, description, amount (dollars), trial_days, setup_fee, cycles, active, public
  - returns: { ok, note }
- `DELETE /api/plans/:id` — Delete a plan with no active subscribers.
  - returns: { ok }
- `PATCH /api/subscriptions/:id` — Change a subscription's status (pause/cancel), amount, next billing date or notes.
  - send: status (active|paused|canceled|past_due|trialing), amount (dollars), next_billing_at, notes
  - returns: { ok }
- `POST /api/subscriptions/:id/charge` — Charge the subscriber's saved card now and email a receipt (or a failure notice). **[moves money]** **[contacts customers]**
  - returns: { ok, message? }
- `POST /api/costs` — Add a workspace expense (software, hosting, domain) (owner/admin).
  - send: site_id, name, kind, vendor, cycle (weekly|monthly|yearly), amount (dollars), status, renews_on, seats, account_email, manage_url, notes
  - returns: { id }
- `PATCH /api/costs/:id` — Update a workspace expense (send all fields) (owner/admin).
  - send: same as create
  - returns: { ok }
- `DELETE /api/costs/:id` — Delete a workspace expense (owner/admin).
  - returns: { ok }
- `GET /api/tax-certificates` — List dealer resale/tax-exemption certificates.
  - send: query: status, dealer
  - returns: { certificates }
- `POST /api/tax-certificates` — Add a tax certificate for a dealer and refresh their tax-exempt status.
  - send: dealer_id (required), state, number, legal_name, file (data URL), file_name, expires_on, status (pending|approved), notes
  - returns: { id, taxExempt }
- `PATCH /api/tax-certificates/:id` — Approve/reject/update a tax certificate; approving or rejecting emails the dealer unless notify:false. **[contacts customers]**
  - send: status (pending|approved|rejected|expired), state, number, expires_on, notes, notify
  - returns: { ok, taxExempt }
- `DELETE /api/tax-certificates/:id` — Delete a tax certificate and refresh the dealer's tax-exempt status.
  - returns: { ok }

### Abandoned carts & waitlist (`carts`)
Cart recovery, reminders, waitlist.

- `GET /api/carts` — Abandoned carts (open ones idle over an hour by default) with stats and recovery settings.
  - send: query: site, status (open|dismissed|recovered|converted), q
  - returns: { carts, stats, recovery, emailReady }
- `PATCH /api/carts/:id` — Set an abandoned cart's status.
  - send: status (open|dismissed|recovered|converted)
  - returns: { ok }
- `POST /api/carts/remind` — Email reminder messages to the shoppers of selected carts (optionally with a coupon). **[contacts customers]**
  - send: ids [] (required), coupon, subject, message
  - returns: { ok, sent, skipped, error }
- `GET /api/sites/:id/cart-recovery` — Get automatic cart-recovery email settings.
  - returns: { enabled, steps: [{hours, coupon}] }
- `PUT /api/sites/:id/cart-recovery` — Turn automatic cart-recovery emails on/off and set up to 4 steps; enabling makes customers receive automatic emails. **[contacts customers]**
  - send: enabled (bool), steps: [{hours, coupon}]
  - returns: { ok }
- `GET /api/waitlist` — Back-in-stock waitlist entries and groups per product/list.
  - send: query: site, status
  - returns: { entries, groups, emailReady }
- `POST /api/waitlist/notify` — Email waitlisted shoppers that a product is available (by ids, or everyone waiting on a product/list). **[contacts customers]**
  - send: ids [] | site_id + (product_id | list)
  - returns: { ok, sent, total }
- `PATCH /api/waitlist/:id` — Set a waitlist entry's status.
  - send: status (waiting|notified|purchased|removed)
  - returns: { ok }
- `DELETE /api/waitlist/:id` — Remove a waitlist entry (status becomes removed).
  - returns: { ok }

### Customers, CRM & dealers (`crm`)
Contacts, deals, tasks, customers, dealers.

- `GET /api/customers` — Everyone who ordered or wrote in, merged by email, with order totals and message counts.
  - returns: { customers: [{email, name, phone, sites, orders, spent, messages, first, last}] }
- `GET /api/crm/contacts` — Search CRM contacts (100 per page) with source and tag counts.
  - send: query: q, tag, source, site, page
  - returns: { contacts, total, page, sources, tags }
- `POST /api/crm/contacts` — Create a CRM contact (409 if the email exists).
  - send: name, email, phone, company, title, source, tags [], notes, address {}, owner, unsubscribed, site_id
  - returns: { id }
- `GET /api/crm/contacts/:id` — Contact with deals, activities, orders, inbox messages and linked dealers.
  - returns: { contact, deals, activities, orders, inbox, dealers }
- `PATCH /api/crm/contacts/:id` — Update a contact.
  - send: name, email, phone, company, title, source, tags, notes, address, owner, unsubscribed
  - returns: { ok }
- `DELETE /api/crm/contacts/:id` — Delete a contact and its activities (deals are kept, unlinked).
  - returns: { ok }
- `GET /api/crm/deals` — List deals in the sales pipeline and the stage names.
  - returns: { deals, stages }
- `POST /api/crm/deals` — Create a deal.
  - send: title, value (cents), stage (lead|qualified|proposal|negotiation|won|lost), probability, expected_close, notes, owner, contact_id, site_id
  - returns: { id }
- `PATCH /api/crm/deals/:id` — Update a deal (changing stage sets a default probability).
  - send: title, value (cents), stage, probability, expected_close, notes, contact_id, lost_reason
  - returns: { ok }
- `DELETE /api/crm/deals/:id` — Delete a deal.
  - returns: { ok }
- `GET /api/crm/tasks` — Open tasks and recent non-task activities.
  - returns: { tasks, recent }
- `POST /api/crm/activities` — Log a note, call, email, meeting or task (logging only; nothing is sent).
  - send: type (note|call|email|meeting|task), subject, notes, due_at, contact_id, deal_id, dealer_id
  - returns: { id }
- `PATCH /api/crm/activities/:id` — Mark an activity/task done or not done.
  - send: done (bool)
  - returns: { ok }
- `DELETE /api/crm/activities/:id` — Delete an activity.
  - returns: { ok }
- `GET /api/dealers` — List dealers with order counts, sales and status counts.
  - send: query: site, status (applied|active|inactive|declined), q
  - returns: { dealers, counts }
- `POST /api/dealers` — Create a dealer account (active by default) (owner/admin).
  - send: site_id (required), company, contact_name, email, phone, status, price_level, payment_terms, tax_exempt, tax_id, credit_limit (cents), website, business_type, notes, address {}
  - returns: { id }
- `GET /api/dealers/:id` — Dealer with orders, activities, portal login and the site's price levels.
  - returns: { dealer, orders, activities, login, levels }
- `PATCH /api/dealers/:id` — Update a dealer (status active grants dealer pricing; price_level sets their discount) (owner/admin).
  - send: company, contact_name, email, phone, status, price_level, payment_terms, tax_exempt, tax_id, credit_limit (cents), website, business_type, notes, address
  - returns: { ok }
- `DELETE /api/dealers/:id` — Delete a dealer (their portal login loses dealer access) (owner/admin).
  - returns: { ok }

### Inbox & live chat (`inbox`)
Form messages, leads, live chat, chat assistant settings.

- `GET /api/forms` — List every form across the workspace's sites (Hubbase Forms) with status and response counts.
  - returns: { forms }
- `GET /api/forms/templates` — Form templates (dealer application, contact, quote, surveys, registrations …).
  - returns: { templates }
- `POST /api/sites/:id/forms/new` — Create a form from a template or SurveyJS JSON (starts as draft).
  - send: template (id) | json (SurveyJS), name
  - returns: { form }
- `GET /api/sites/:id/forms/:formId` — Get a form: SurveyJS json + hb settings (action, notify, payment …).
  - returns: { form, site }
- `PUT /api/sites/:id/forms/:formId` — Update a form (name, status draft|live, json, hb).
  - send: name, status, json, hb
  - returns: { form }
- `GET /api/sites/:id/forms/:formId/responses` — A form's responses (add ?format=csv for CSV).
  - returns: { responses }
- `GET /api/inbox` — List inbox items (forms, dealer applications, warranty, tech support, chat handoffs); spam hidden unless asked.
  - send: query: site, type, status (new|open|waiting|done|spam), q
  - returns: { items, types }
- `PATCH /api/inbox/:id` — Set an inbox item's status, internal notes or assignee (nothing is sent).
  - send: status (new|open|waiting|done|spam), notes, assignee
  - returns: { ok }
- `DELETE /api/inbox/:id` — Delete an inbox item.
  - returns: { ok }
- `POST /api/inbox/bulk` — Set the status of up to 300 inbox items.
  - send: ids [], status
  - returns: { ok }
- `GET /api/inbox/:id/messages` — Ticket thread for an inbox item and the customer's ticket link.
  - returns: { messages, url }
- `POST /api/inbox/:id/messages` — Reply on a ticket; unless internal:true or notify:false the customer is emailed the reply. **[contacts customers]**
  - send: body, attachments [{data, name}], internal (bool), status, notify
  - returns: { ok, email, ref }
- `POST /api/inbox/:id/flag` — Flag an inbox item and set its priority.
  - send: flagged (bool), priority (low|normal|high|urgent)
  - returns: { ok }
- `GET /api/chats` — List live-chat conversations (closed hidden unless asked).
  - send: query: site, status (open|waiting|closed), mode (chat|tech)
  - returns: { chats, unread, aiReady }
- `GET /api/chats/:id` — Get a chat's messages and mark it read.
  - returns: { chat, messages }
- `POST /api/chats/:id/reply` — Send a message to the visitor in a live chat as the team. **[contacts customers]**
  - send: body
  - returns: { ok }
- `POST /api/chats/:id/suggest` — AI-suggest a reply for a chat (not sent).
  - returns: { suggestion }
- `PATCH /api/chats/:id` — Set a chat's status or turn its AI auto-replies on/off.
  - send: status (open|waiting|closed), ai (bool)
  - returns: { ok }
- `DELETE /api/chats/:id` — Delete a chat and its messages.
  - returns: { ok }
- `GET /api/sites/:id/assistant` — Get the site's chat assistant settings.
  - returns: { assistant: {enabled, ai, techAi, name, greeting, facts}, aiReady }
- `PUT /api/sites/:id/assistant` — Save chat assistant settings (send all fields); applies to the live site immediately (owner/admin).
  - send: enabled, ai (off|away|always), techAi, name, greeting, facts
  - returns: { ok }

### Email (`email`)
Email templates, email log, test sends.

- `GET /api/email` — Workspace email provider settings (secrets masked), all templates and the recent email log.
  - returns: { settings, provider, envProvider, templates: [{key, label, group, vars, subject, html}], log }
- `PUT /api/email` — Save the workspace email provider and sender (owner/admin).
  - send: provider (resend|sendgrid|postmark|smtp|""), fromName, fromEmail, replyTo, bcc, apiKey, smtpHost, smtpPort, smtpUser, smtpPass
  - returns: { ok, provider }
- `POST /api/email/test` — Send a test email to an address (defaults to the signed-in user) (owner/admin). **[contacts customers]**
  - send: site_id, to, template
  - returns: { ok, ... }
- `GET /api/sites/:id/emails` — The site's email template overrides and sender name/reply-to.
  - returns: { overrides, from }
- `PUT /api/sites/:id/emails` — Set the site's sender name and reply-to address.
  - send: fromName, replyTo
  - returns: { ok }
- `PUT /api/sites/:id/emails/:template` — Override one email template for the site; used for all future customer emails.
  - send: subject, html, active
  - returns: { ok }
- `DELETE /api/sites/:id/emails/:template` — Remove the site's override so the default template is used.
  - returns: { ok }
- `GET /api/sites/:id/emails/:template/preview` — Preview a template rendered with sample data as HTML (POST returns JSON).
  - returns: text/html (GET) or { subject, html } (POST)

### Market research (`research`)
Prospect discovery, enrichment, dealer location.

- `GET /api/research/config` — Research setup: Google Maps availability, prospect categories, dealers on the map, status counts and past sessions.
  - returns: { google, categories, dealers, counts, sessions }
- `PUT /api/research/categories` — Replace the prospect search categories.
  - send: categories: [{key, label, queries []}]
  - returns: { ok }
- `POST /api/research/discover` — Search Google Maps for prospect businesses in an area and save them as prospects (uses Maps API quota).
  - send: city, region, country, bounds {north, south, east, west}, categories [keys], query
  - returns: { found, session, center, bounds }
- `GET /api/research/prospects` — List prospects.
  - send: query: status, category, session, q, north, south, east, west
  - returns: { prospects }
- `GET /api/research/export.csv` — Download all prospects as CSV.
  - returns: text/csv
- `PATCH /api/research/prospects/:id` — Set a prospect's status or notes.
  - send: status (new|reviewing|qualified|rejected|crm), notes
  - returns: { ok }
- `DELETE /api/research/prospects/:id` — Delete a prospect.
  - returns: { ok }
- `POST /api/research/prospects/:id/enrich` — Fetch the prospect's website to find emails, phones and social links.
  - returns: { prospect }
- `POST /api/research/enrich` — Enrich up to 15 prospects from their websites.
  - send: ids []
  - returns: { done }
- `POST /api/research/convert` — Turn prospects into CRM contacts with a lead-stage deal each.
  - send: ids [], site_id
  - returns: { created, skipped }
- `POST /api/research/locate-dealers` — Geocode up to 40 dealer addresses so they appear on the research map.
  - returns: { placed, remaining }

### Analytics (`analytics`)
Dashboard totals and traffic (read only).

- `GET /api/me` — Who is signed in, their workspaces, the current workspace (role, plan, site access, settings) and platform info.
  - returns: { user: {id, email, name, platformAdmin}, workspaces: [{id, name, role}], workspace: {id, name, plan, role, siteIds, settings}, platform: {signupOpen, sitesDomain, hubHost, media} }
- `GET /api/overview` — Dashboard totals for the last N days: orders, revenue, messages, page views per day and per site, plus recent activity.
  - send: query: days (1-365, default 30)
  - returns: { days, totals: {orders, revenue, messages, views, unread, openOrders, sites, live}, series: [{day, orders, revenue, messages, views}], sites: [...], activity: [...], currency }
- `GET /api/analytics` — Page-view analytics: views per day per site, top pages and top referrers.
  - send: query: days (1-365), site
  - returns: { days, series: [{day, site_id, v}], pages: [{site_id, path, v}], refs: [{ref, v}] }

### Store settings (`store_settings`)
Tax, shipping, checkout/commerce settings.

- `GET /api/sites/:id/commerce` — Get the site's commerce settings (payments mode, tax, shipping rules, review requests, URLs).
  - returns: { commerce }
- `PUT /api/sites/:id/commerce` — Change commerce settings; only keys sent change, and they apply to the live store immediately (owner/admin).
  - send: shippingRules [{name, cost, freeOver, days, pickup, pickupDiscountPct}], reviewsAutoPublish, taxRate, payments (request|stripe|card), autoWaitlist, reviewRequests, reviewRequestDays, urls {product, cart, orderConfirmation, account}
  - returns: { ok }
- `GET /api/sites/:id/tax` — Get tax settings.
  - returns: { taxRate, taxStates, originState, taxShipping }
- `PUT /api/sites/:id/tax` — Replace tax settings; applies to live checkout immediately.
  - send: taxRate (%), taxStates {TX: 8.25}, originState, taxShipping
  - returns: { ok }
- `GET /api/sites/:id/shipping` — Get shipping rules and available zones.
  - returns: { rules, pickupDiscountPct, zones }
- `PUT /api/sites/:id/shipping` — Replace ALL shipping rules; applies to live checkout immediately.
  - send: rules: [{name, rateType (free|flat|percent|per_item|weight), cost, freeOver, min, max, zones [], states [], days, pickup, pickupDiscountPct, active, audience (all|retail|dealer)}]
  - returns: { ok }

### Workspace admin (`admin`)
Workspace settings and anything not covered above.

- `PATCH /api/workspace` — Update workspace name and settings (notification email, webhook, currency, timezone); owner/admin only.
  - send: name, notifyEmail, webhookUrl, currency, timezone, stripeSecret (do not change)
  - returns: { ok }
- `POST /api/sites/:id/rotate-key` — Generate a new public site key; the old key stops working immediately, breaking any connected site still using it (owner/admin).
  - returns: { public_key }
- `GET /api/costs` — List what the workspace pays for (software, hosting, domains) with the monthly total in cents.
  - returns: { costs: [{id, site_id, name, kind, vendor, cycle, amount, status, renews_on, seats, account_email, manage_url, notes}], monthly }


## 6. Site build format

# Hubbase site build format

A builder site ("kind": "built") is stored as a **draft build** plus a **published snapshot**.
Editing the draft never changes the live site until it is published.

## GET /api/sites/:id/build

Returns:

```json
{
  "site": { "id": "site_x", "name": "...", "slug": "...", "domain": "...", "kind": "built", "status": "live", "published_at": "..." },
  "theme": { "colors": { "primary": "#..", "accent": "#..", "bg": "#..", "surface": "#..", "text": "#..", "muted": "#..", "dark": "#.." },
             "fonts": { "heading": "Plus Jakarta Sans", "body": "Inter" },
             "radius": 14, "buttonStyle": "solid|pill|outline|sharp", "headingWeight": 700, "headingCase": "none|uppercase",
             "headingTracking": -0.02, "baseSize": 17, "maxWidth": 1180, "shadow": "none|soft|strong",
             "spacing": "compact|normal|airy", "customCss": "" },
  "settings": {
    "header": { "logoText": "", "logoImage": "", "layout": "classic|centered|minimal|store", "style": "light|dark|transparent", "sticky": true,
                "customLinks": false, "links": [{ "label": "", "href": "" }], "cta": { "label": "Contact", "href": "/contact" }, "showCart": true, "showAccount": true },
    "announcement": { "on": false, "text": "", "href": "" },
    "footer": { "layout": "columns", "tagline": "", "columns": [], "social": [], "copyright": "", "dark": true },
    "seo": { "title": "", "description": "", "image": "" },
    "commerce": { "currency": "USD", "payments": "request", "taxRate": 0, "...": "..." },
    "favicon": ""
  },
  "pages": [
    { "id": "pg_x", "path": "/", "title": "Home", "sort": 0, "in_nav": 1,
      "seo": { "title": "", "description": "", "image": "" },
      "blocks": [ { "id": "b_x", "type": "hero", "title": "...", "primary": { "label": "Shop", "href": "/shop" }, "style": {} } ],
      "published": true, "updated_at": "..." }
  ],
  "products": [ "active products (read-only here; edit via /api/products)" ],
  "categories": [ "categories (read-only here)" ],
  "posts": [ "published blog posts (read-only here)" ]
}
```

- `products`, `categories`, `posts` and `site` are context for rendering. They are ignored by PUT; change them with their own endpoints.
- Settings keys owned by other screens (payments, dealerPortal, emailFrom, cartRecovery, reviewRequests, autoWaitlist, assistant, affiliates, email) are always kept from the server copy, whatever you send. Editors (non owner/admin) cannot change `commerce` or product-page fees.

## PUT /api/sites/:id/build  (save the draft)

Body: `{ "theme": {...}, "settings": {...}, "pages": [ {id?, path, title, in_nav, seo, blocks}, ... ] }`

- It **replaces the whole draft**: every page not in `pages` is **deleted**, and theme/settings are replaced. Always send everything.
- Page order in the array becomes the nav/sort order. `path` is normalised (lowercase, leading `/`); every path must be unique and a home page `/` is required. Max 150 pages, about 7 MB total.
- Pages keep their id when you send the existing `id`; a page without a known id is created with a new id. The response lists `{id, path}` for every page.
- `in_nav` true/1 shows the page in the header menu.
- Saving is draft-only. Nothing changes on the live site.

## Publishing

- `POST /api/sites/:id/publish` copies the current draft (theme, settings, every page's blocks and SEO) to the live site. You may pass the full build in the same body to save and publish in one call.
- **Connected sites** (`kind: "connected"`, still served from their own host) cannot publish: publish returns 409 and the Hubbase build stays a private preview until an owner/admin calls `POST /api/sites/:id/go-live`, which switches the domain to Hubbase and publishes.
- Publishing is visible to customers immediately. Only publish when the user asked for it.

## Safe edit recipe (edit one block)

1. `GET /api/sites/:id/build`.
2. Find the page by `path`, then the block by `id` (or `type`).
3. Change only the fields you need on that block object; keep its `id`, `type` and `style`.
4. `PUT /api/sites/:id/build` with `{ theme, settings, pages }` exactly as fetched, plus your change. Never send a partial pages list.
5. Re-GET to confirm, and publish only if asked.

## Add a page

Append to `pages` (no `id`): `{ "path": "/about", "title": "About", "in_nav": true, "seo": { "title": "About us", "description": "..." }, "blocks": [ ... ] }`, then PUT the whole build.
To add a block, insert an object `{ "id": "b_<random>", "type": "<type>", ...props, "style": {} }` into a page's `blocks` array at the position you want (blocks render top to bottom). Give each block a unique id (e.g. `b_` + 8 random letters/digits); copies must never share ids.

## Block object

Every block is `{ id, type, ...content props, items?: [...], style: {...}, hidden?: true }`.
Text props accept plain text; `html`/`body` props accept simple HTML (b, strong, i, em, u, a, br, p, ul, ol, li, h2-h4, blockquote, span, mark, code, small, sup, sub, hr); anything else is stripped.
Links (`href`) are site paths like `/products/<slug>`, `/collections/<slug>`, `/blog/<slug>`, `/contact`, or full https URLs. Images are URLs (`/media/...` or https) or a placeholder like `gradient:sunset`.
Buttons are objects `{ label, href }`.

Common `style` props (all optional): `bg` (default | surface | soft | primary | accent | dark | gradient | image | custom), `bgImage` (with bg image), `bgColor` (with bg custom), `textColor`, `overlay` (0-90, for image backgrounds), `pad` (none | sm | md | lg | xl), `width` (normal | narrow | wide | full), `align`, `anim` (none | up | zoom | left), `anchor` (section id for #links), `hideMobile`, `hideDesktop`, `className`.
`hidden: true` keeps a block in the draft but hides it on the site.

## Block types (type key: main props; items[] shows the fields of each item)

Heroes
- `hero`: layout (split | split-reverse | centered | background | minimal), height (auto | tall | full), badge, eyebrow, title, subtitle, primary {label, href}, secondary {label, href}, note, image
- `page_header`: eyebrow, title, subtitle
- `hero_slider`: height (px), interval (seconds), items[] {image, badge, title, subtitle, text, cta, href, cta2, href2}

Content
- `rich_text`: html
- `image_text`: side (left | right), eyebrow, title, body (HTML), items[] {text} (bullet list), button {label, href}, image
- `features`: eyebrow, title, subtitle, columns, look (cards ...), items[] {icon, title, text}
- `team`: eyebrow, title, subtitle, columns, items[] {name, role, photo, bio}
- `steps`: eyebrow, title, subtitle, layout (horizontal ...), items[] {title, text}
- `blog_posts`: eyebrow, title, subtitle, limit, more {label, href}
- `documents`: eyebrow, title, subtitle, search (bool), items[] {title, url, group}
- `table`: eyebrow, title, subtitle, columns ("A | B"), highlight, filter (bool), linkLabel, note, items[] {cells}
- `doc_library`: eyebrow, title, subtitle, types, extra[]

Media
- `gallery`: eyebrow, title, subtitle, layout (grid ...), columns, items[] {image, caption}
- `video`: eyebrow, title, subtitle, url (YouTube, Vimeo or video file)
- `video_gallery`: eyebrow, title, subtitle, columns, items[] {title, url, text}
- `map`: title, address, height, zoom

Social proof
- `stats`: title, look, items[] {value, label}
- `logos`: title, motion (static ...), items[] {name}
- `testimonials`: eyebrow, title, layout (cards ...), stars (bool), items[] {quote, name, role, avatar}
- `quote`: quote, author

Commerce
- `products`: eyebrow, title, subtitle, source (all ...), category, limit, columns, layout (grid ...), showButton (bool), more {label, href}
- `product_spotlight`: product (product id/slug), eyebrow, title, body (HTML), side (left | right)
- `menu`: eyebrow, title, subtitle, columns, items[] {category, name, price, description}
- `category_grid`: eyebrow, title, subtitle, parent, only, columns, look (tiles ...)
- `parts_diagram`: title, subtitle, image, items[] {no, part, text, qty, product, x, y}
- `promo_tiles`: eyebrow, title, subtitle, columns, height (tall ...), items[] {image, badge, title, text, cta, href}
- `collection_showcase`: eyebrow, title, body, highlights (one per line), more {label, href}, mosaic[] {image, label, href}, items[] {image, title, href}
- `product_selector`: eyebrow, title, subtitle, skus
- `parts_finder`: eyebrow, title, subtitle, collection
- `store_locator`: eyebrow, title, subtitle, center ("lat, lng"), featured[]
- `configurator`: title, subtitle, src (embed URL), height
- `link_cards`: eyebrow, title, subtitle, columns, look (card | overlay ...), ratio (e.g. 1/1), items[] {image, title, text, href, cta}
- `collection_listing`: mode (show ...), note (placeholder where a collection page lists its products)

Conversion
- `pricing`: eyebrow, title, subtitle, note, items[] {name, price, period, description, features, cta, href}
- `faq`: eyebrow, title, subtitle, layout (single ...), items[] {q, a}
- `cta`: layout (card ...), title, subtitle, primary {label, href}, secondary {label, href}
- `newsletter`: title, subtitle, placeholder, button, success, note
- `contact_form`: eyebrow, title, subtitle, formName, button, success, showInfo (bool), email, phone, address, hours, items[] {label, type, required, half} (form fields)
- `comparison`: title, subtitle, columns ("Us, Typical option"), highlight, items[] {feature, values}
- `countdown`: title, subtitle, date (ISO), ended, button {label, href}
- `app_badges`: eyebrow, title, subtitle, items[] {store (chrome | firefox | edge | safari | appstore | googleplay | custom), href}
- `form`: formId (a Hubbase Forms id, frm_…), eyebrow, title, subtitle, card (card|plain)
- `dealer_apply`: eyebrow, title, subtitle, types (comma list), button, success
- `lookup`: kind (order ...), eyebrow, title, subtitle, button

Layout
- `marquee`: text, speed
- `columns`: title, columns, items[] {title, html}
- `spacer`: size (px), line (bool)
- `sticky_nav`: title, items[] {label, href}, cta {label, href}
- `tabs`: eyebrow, title, subtitle, items[] {label, html}

Advanced
- `html`: code (custom HTML), height

Unknown `type` values are skipped when rendering, so use only the keys above.


## 7. Errors

- 400 — the request is missing something; the message says what.
- 401 — the key is unknown or revoked.
- 403 — not allowed: the message names the permission, switch or site setting involved.
- 404 — the record doesn't exist or isn't on a site you may use.
- 409 — the action doesn't fit the current state (e.g. publishing a site that still runs on its own host — it must go live first).
