Pages & Sites
Space MCP tools in the Pages & Sites scope (pages). Reach these with a wks_live_ key over space-mcp or space-api.
Tools in the pages scope, callable over space-mcp (tools/call) or space-api (REST) with a wks_live_* key that grants pages.
add_page_media
Scope: pages:write
Put a picture, GIF, video, audio (music, a podcast clip, a voice note) or a YouTube / Vimeo / Loom video on a page, in the section you name, and say how it plays. Give asset_id (a library file of this space — list_document_assets, upload_asset / add_asset_from_url first; the page follows the file when it changes) or url (a file's link, or the YouTube/Vimeo/Loom page link). Where: section = a section's id, its number (1 = first) or words from its heading (list_page_contents → sections), position start|end (inside it) or before|after it; no section = at the end of the page, before the footer. How it plays: play "click" (waits for the visitor — default), "auto" (as the page opens) or "in_view" (when the visitor scrolls to it, pausing when they scroll away); loop; sound (a video that plays by itself is silent unless sound: true); controls; start_at, volume, speed; a thumbnail (thumbnail_asset_id or thumbnail_url — a video's poster, an audio's cover, an embed's still; YouTube's is automatic). Browsers hold sound until the visitor has touched the page: a video then starts muted with its controls, audio on their first tap — the answer says so. Returns the media's id for set_page_media / remove_page_media.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
asset_id | string | A library file of this space (picture, GIF, video or audio). | |
url | string | Or a link: a file's url, or a YouTube / Vimeo / Loom page link. | |
kind | image | video | audio | embed | Read from the file when left out. | |
section | string,integer | A section's id, its number (1 = first) or words from its heading. | |
position | start | end | before | after | Inside the section at its start/end, or right before/after it. Default end. | |
play | click | auto | in_view | ||
loop | boolean | ||
sound | boolean | Video / embed: with sound. Audio always has sound. | |
controls | boolean | Show the player's controls (default true). | |
start_at | number,string | Where playing starts: seconds (90) or a time ("1:30"). | |
volume | number | Loudness 0–100 (video/audio files; a YouTube/Vimeo/Loom player sets its own). | |
speed | number | Playback speed 0.25–2 (video/audio files). | |
thumbnail_asset_id | string | A picture from the library shown before it plays. | |
thumbnail_url | string | Or a picture's link. | |
caption | string | A line under it (audio: its title beside the player). | |
alt | string | What a picture shows, for people who can't see it. | |
width | full | wide | narrow | Default wide (audio: narrow). |
book_on_page
Scope: pages:write
Book someone into an OPEN slot on a booking page, exactly as if they booked it themselves: the booking lands on the calendar, a meeting link is made if the page uses one, the guest becomes a contact and gets the page's confirmation email. Find open slots with preview_booking_page, then pass its start_at (or date + start in the page's timezone). A page that asks questions (a form) needs its required answers in form_data.
| Parameter | Type | Required | Description |
|---|---|---|---|
booking_page_id | string | yes | |
start_at | string | A slot's start_at from preview_booking_page (ISO 8601). | |
date | string | Or: YYYY-MM-DD in the page's timezone (with start). | |
start | string | HH:MM in the page's timezone (with date). | |
guest_name | string | yes | |
guest_email | string | yes | |
guest_notes | string | ||
form_data | object | Answers to the page's form, by field key (get_booking_page lists its questions). |
cancel_booking
Scope: pages:write
Cancel a booked appointment (status → cancelled). Get the booking_id from list_bookings. The guest is emailed that it is cancelled (with a calendar file that removes it) unless notify_guest:false; add a note to say why. The linked calendar event is kept unless delete_calendar_event:true. Returns guest_told.
| Parameter | Type | Required | Description |
|---|---|---|---|
booking_id | string | yes | |
delete_calendar_event | boolean | Also remove the linked calendar event (default false, matching the in-app cancel). | |
notify_guest | boolean | Email the guest (default true), with a calendar file that updates their calendar. false = say nothing. | |
note | string | A line for the guest in that email — why, or what happens next. |
create_blog_author
Scope: pages:write
Create a hand-typed byline the space owns — a guest writer, a client principal, a team name like "The Silverpoint Dental Team". Reuse it across posts via author_id. To credit a real Wakato member or business account instead, ask the owner to add them in the Blog UI: attaching a real person's identity to content is their call, not yours.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | Byline as it should read, e.g. "Dr. Evan Lane, DDS". |
bio | string | Short bio shown under the byline on the post. | |
avatar_url | string | Hosted headshot/logo URL. | |
socials | object | Optional links, e.g. {"website":"https://…","linkedin":"https://…"}. |
create_blog_collection
Scope: pages:write
Create a blog (collection) — the REAL blog system, not a static page. NEVER hand-write post cards or one page per post: pass index_page_id and the blog section is written into that page for you, so every post you publish afterwards appears there automatically. To make it part of a website, pass site_id from create_site/list_sites. A blog holds the post layout (template_html tokens: {{title}} {{excerpt}} {{body}} {{date}} {{category}} {{cover_image}} (a full <img> or nothing) {{author}} (the NAME only) plus {{author_avatar}} {{author_bio}} {{author_links}} — each empty when the author has none, so an author block collapses instead of breaking — or {{author_card}} for the whole byline as one unit; a neutral default is used if omitted). Idempotent by base_slug per space. Then add posts with create_blog_post.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Blog name, e.g. "Acme Journal" (default "Blog"). | |
site_id | string | Attach to a site so the blog is part of one website (recommended). | |
base_slug | string | URL segment, default "blog" (derived from the name if omitted). Unique per space, fixed at creation. | |
template_html | string | Custom post layout HTML with the six tokens. Omit for a neutral responsive default. | |
index_page_id | string | The page this blog shows on. The blog section is written into that page automatically — do not add cards yourself. | |
card_template | string | HTML for ONE index card, using {{url}} {{title}} {{excerpt}} {{cover_image}} {{author}} {{date}} {{category}}. Design it however the site needs — the same markup renders every post, forever. {{cover_image}} is a complete <img class="card-cover"> or NOTHING when the post has no cover, so never wrap it in your own <img>; for a background/overlay treatment use {{cover_image_css}} (the whole background-image declaration, or nothing) rather than url('{{cover_image_url}}'), which would emit url('') on a coverless post. Match the site's own card design — cards then render bare so the host page's grid styles them. Omit for a neutral built-in card. |
create_blog_post
Scope: pages:write
Write a blog post into a collection. Provide body_html (HTML rendered into the blog template), plus optional excerpt, cover_image_url, and author. Defaults to a draft; pass status:'published' to publish immediately (live at /post/<id>). A published post appears on the blog's index page on its own — never create a page for a post. Requires create_blog_collection first.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection_id | string | yes | From create_blog_collection / list_blog_collections. |
title | string | yes | |
body_html | string | Post body as HTML. | |
excerpt | string | 1–2 sentence summary; also the default meta description and card text. | |
cover_image_url | string | Hosted cover image URL. | |
author_id | string | A byline from list_blog_authors. Preferred over author — it carries the photo, bio and links. | |
author | string | Fallback byline text when no author_id is used. | |
slug | string | Preferred URL slug (a short suffix is appended to keep it unique). | |
status | draft | published | Default draft. | |
category | string | Short label shown on index cards, e.g. "Compliance". | |
meta_title | string | SEO <title> override (≤70 chars). Defaults to title. | |
meta_description | string | SEO meta description override (≤160 chars). Defaults to excerpt. | |
og_image_url | string | Social share image override. Defaults to cover_image_url. |
create_booking_page
Scope: pages:write
Create a real Space booking page so visitors can schedule time. Returns { booking_page_id, slug, calendars }. Embed with <div data-wakato-booking="<booking_page_id>"> or link a CTA to /b/<slug>. Active immediately; open hours default to Mon–Fri 9–5 unless you pass availability. Every calendar of the space closes its slots unless you pass checks_calendars. Set meeting_location_type to "wakato_meet" so EVERY booking automatically creates a Wakato Meet video-call link.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | yes | |
description | string | ||
booking_type | one_on_one | group | ||
duration_minutes | number | ||
timezone | string | ||
meeting_location_type | wakato_meet | in_person | phone_call | custom_link | Where meetings happen. "wakato_meet" auto-creates a Wakato Meet video link on every booking. | |
meeting_location_value | string | Address (in_person), phone/instructions (phone_call), or URL (custom_link). Leave empty for wakato_meet. | |
form_id | string | Attach a Space form (from list_forms/create_form) so guests answer extra questions when booking. Must belong to this space. | |
availability | object[] | Weekly open hours, replacing the current ones: [{ day: 1, start: "09:00", end: "17:00" }] — day 0 = Sunday … 6 = Saturday; several ranges on one day are several rows. | |
date_overrides | object[] | Days that differ from the weekly hours (replaces the list): { date: "2026-12-24", available: false } closes a day; { date, available: true, ranges: [{ start, end }] } opens it with its own hours. | |
max_attendees | number | Group pages: how many people can book the same slot. | |
slot_interval_minutes | number,null | How often slots start (e.g. 15 = every quarter hour); null = every duration_minutes. | |
checks_calendars | string[] | Which calendars' busy events close slots on this page — keys from list_calendars ('space' = events made in this space, 'conn:<id>' = a Google/Outlook account, 'phone:<id>' = a member's phone). The others are ignored. [] = only this page's own bookings count. | |
copy_bookings_to | string,null | Also put every booking on this Google/Outlook calendar of the space (a 'conn:<id>' key from list_calendars), so it shows as taken there too. null = stop. | |
branding | object | Partial branding patch, MERGED onto existing branding (null value clears a key). Keys: mode ('light'|'dark'), primary_color, background_color, card_color, text_color, muted_color, border_color, on_primary_color, input_background, subtle_color, disabled_color, font_family, radius, button_text, photo_url (https cover photo shown on the public page's brand panel), logo_url. Setting only photo_url never clobbers existing colors. |
create_page
Scope: pages:write
Create a new AI-designed web page. EVERY page is part of exactly one artifact — decide FIRST: (a) part of a browseable WEBSITE → create_site kind:"website" and pass site_id; (b) a step of a LANDING flow/funnel → create_site kind:"landing" and pass site_id (steps link FORWARD only, no nav); (c) a single standalone LANDING PAGE → no site_id (pass standalone:true if it's your 2nd+ within the hour). Pass layout_html (a complete HTML document) to build it in one call. Any <form> must use the binding <form data-wakato-form-id="<id>"> and any calendar must use <div data-wakato-booking="<id>"> (or a /b/<slug> link) referencing a real Space form/booking page — create those first with create_form/create_booking_page. Storefront sections: embed <div data-wakato-products></div> (live grid of every product listed on this space, optional data-limit="N"). For CATEGORY sections (Men/Women/Kids…) add data-tags="men" — comma-separated product tags, the grid keeps only products carrying ALL of them, and it stays live as tagged products are added/removed (tag products via create_product/update_product tags). <div data-wakato-product="<product_id>"></div> renders one product's card. Leave every container EMPTY; the platform fills live price/photos/stock/rating with working Buy buttons at render time (restyle every card with set_product_card_template; a Buy button you design yourself works too — any element with data-wakato-checkout="<product_id>", or Wakato.checkout(id) from your own script, opens the checkout over the page; to design the product page AND the checkout form yourself, bound live to the listing, read get_storefront_sdk_reference), and each card links to the product's live DETAIL page at /product/<product_id> (renders automatically — design it with set_product_page_template, never hand-build product pages). Every LIVE product of the store connected to this space is on its storefront automatically — nothing to link.Blog section: <div data-wakato-blog-index data-base="<the blog's base_slug>"></div> — leave it EMPTY; it fills with cards for every published post, forever. data-base MUST equal the collection's base_slug (list_blog_collections) or the section renders empty. Easier: create the page, then set index_page_id on the blog (create_blog_collection / update_blog_collection) and the section is written for you. NEVER hand-write post cards and NEVER create one page per post — both go stale the moment a post is added. Link sibling pages with /pg/<their slug> hrefs. Idempotent by title: a same-title page is filled if empty, otherwise returned untouched. ENFORCED: a 2nd+ standalone page within the hour is rejected unless standalone:true — scattering one website/funnel across loose pages is never allowed.
DESIGN BRIEF — an unstyled or default-looking page is a FAILED build. BEFORE the first page of a conversation, call get_page_guide: it carries the space's brand record and how to work with the person — ask before deciding the look, show a direction, build a DRAFT, check it with preview_page, publish only when they say so — plus what usually converts for each kind of page (get_page_guide {kind}). The person's words, their brand and your instructions come first; never give two businesses the same page. Commit to a small system before writing HTML: one background family, one ink color, ONE accent kept for actions, AA-readable contrast; two Google Fonts via <link> (a display face with character, a plain body face); a type scale and a spacing rhythm, content in a max-width container with ≥16px side gutters. RESPONSIVE IS MANDATORY: include the viewport meta, grids/flex collapse to one column ≤768px, nothing overflows horizontally (images max-width:100%, fixed aspect via object-fit:cover), tap targets ≥44px. IMAGES: reference ONLY real URLs you know exist (space assets, generate_image results) — NEVER invent file paths; with no image available, design with gradients/patterns/typography instead of leaving broken tiles or icon placeholders. Whenever a url came with a library asset id, CARRY THE ID: <img src="<url>" data-wakato-asset="<asset_id>"> (a CSS background puts data-wakato-asset-bg="<asset_id>" on the element). That binds the media to the file, so replacing the file later updates every page using it and the owner can manage it from the page editor's media shelf. Video/audio/YouTube: add_page_media. Popups open by data-wakato-popup-open="<id>". LIVE DATA: to show a space table's rows (pricing, menu, directory, schedule), embed <div data-wakato-table="<table id>" data-columns="key1,key2" data-limit="10"></div> and let it fill itself — NEVER paste rows in as text, which freezes them the moment you write them. Only a table whose owner set web_published renders, and only its web_columns (list_tables tells you both); if the table the user means is not published, say so rather than pasting its rows in. Put a short fallback line inside the container for the case where it resolves to nothing. To show a document's text (terms, a policy, a guide, a changelog), embed <div data-wakato-document="<document id>"></div> — again never pasting the text in, and again only a document whose owner published it to the web renders (list_documents tells you which). Same for products (data-wakato-products) and blog posts (data-wakato-blog-index). Content gated behind scroll-triggered JS must still be visible with JS disabled (headless QA screenshots scroll once, nothing more).
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | yes | |
description | string | ||
layout_html | string | Complete HTML document for the page body. | |
site_id | string | The artifact this page belongs to (from create_site/list_sites). REQUIRED for every website page and every landing-flow step; omit ONLY for a single standalone landing page. | |
standalone | boolean | Set true ONLY for an intentional separate single landing page unrelated to other recent pages. Required for a 2nd+ standalone page within an hour (otherwise rejected to prevent scattering one website/funnel as loose pages). | |
meta_title | string | Search/social <title> override (≤70 chars). Defaults to the page title. | |
meta_description | string | Meta + og:description (≤160 chars). Falls back to description; when neither is set the tag is omitted, leaving any <meta name="description"> in your own layout_html to stand. | |
og_image_url | string | Social share image for this page. |
create_site
Scope: pages:write
Create a web artifact and DECLARE what it is. kind:"website" = a browseable multi-page site (visitors roam freely via shared navigation). kind:"landing" = a landing page flow: forward-only steps toward ONE goal — opt-in → sales → thank-you etc. — with navigation disabled (when someone asks for a 'funnel', this is it; a single conversion page needs NO site — just create_page standalone). REQUIRED before any multi-page build: create the artifact FIRST, then pass its id as site_id on every create_page call (do NOT scatter pages standalone). Idempotent by title. The first page attached becomes the home page / step 1.
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | yes | Brand/offer name, e.g. "Acme Bike Co." or "Spring Sale Funnel". |
kind | website | landing | yes | website = browseable with shared nav · landing = forward-only step flow, nav disabled (a funnel when 2+ steps). |
description | string | ||
nav_layout | top | sidebar | none | Nav style for websites (default top). Ignored for kind:landing — landing flows never get a nav. | |
logo_url | string | Optional brand logo URL. |
delete_booking_page
Scope: pages:write
PERMANENTLY delete a booking page (its booking history is deleted with it). Refused while upcoming confirmed bookings exist — cancel those first with cancel_booking. Prefer update_booking_page status:"paused" to just stop new bookings.
| Parameter | Type | Required | Description |
|---|---|---|---|
booking_page_id | string | yes |
delete_page
Scope: pages:write
PERMANENTLY delete one page (a published page goes offline). To remove a WHOLE website or landing flow use delete_site instead — it removes the artifact and all its pages in one call. If the deleted page belonged to a site, run validate_site afterwards and fix any nav item that pointed at it.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes |
delete_site
Scope: pages:write
PERMANENTLY delete a web artifact — the WHOLE website or landing flow: by default every page/step is deleted with it (published pages go offline). This is the correct way to remove a website or funnel; deleting its pages one by one leaves the shell behind. Pass keep_pages: true only if the pages should survive as standalone single landing pages (they lose the shared nav/flow).
| Parameter | Type | Required | Description |
|---|---|---|---|
site_id | string | yes | |
keep_pages | boolean | true = detach the site's pages as standalone instead of deleting them (default false). |
edit_page_html
Scope: pages:write
Write or change a page in pieces, for a page too long to send whole in one call. append: add a piece before </body> (or a before marker, or at the end while the page is still open) — so build a long page head, sections, script, then </body></html>, each piece under ~12 KB (a longer one is cut off). find + replace: change one exact piece (replace_all for every match; replace "" removes it). Checked like update_page; a live page changes live.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
append | string | A piece of HTML to add. | |
before | string | With append: put the piece right before this exact text on the page (default: before </body>, or at the end when the page has none yet). | |
find | string | An exact piece of the page's current HTML to change. | |
replace | string | What to put in its place ("" removes it). | |
replace_all | boolean | With find: change every match, not just one. |
get_booking_page
Scope: pages:read
Get a Space booking page including its availability and timezone.
| Parameter | Type | Required | Description |
|---|---|---|---|
booking_page_id | string | yes |
get_page
Scope: pages:read
Get a single page including its layout_html (the full HTML document).
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes |
get_page_analytics
Scope: pages:read
How a page is doing with visitors — the same numbers as the page's Analytics tab: visits, people (and how many came back), time on page, how many left right away, how far they scrolled, forms sent, bookings, who is on it right now, visits by day and busiest hours, the change against the period before, and where they came from (sites, campaigns), where they are (countries, cities, languages) and what they used (devices by name — iPhone, Android phone, Mac…, systems, browsers). Give page_id for one page in full, site_id for a site with a row per page, or nothing for every published page ranked by visits. range: today | 7d | 30d (default) | 90d | all, or from/to dates. Counts only visits a real browser opened.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | One page, in full (list_pages). | |
site_id | string | A site: its totals and a row per page (list_sites). | |
range | today | 7d | 30d | 90d | all | Default 30d. | |
from | string | ISO date — a custom window (with to). | |
to | string | ISO date. |
get_page_guide
Scope: pages:read
READ THIS BEFORE BUILDING A PAGE OR A WEBSITE. Wakato's guide to pages people actually want: who decides (the person, then their brand, then your instructions), how to work with them (ask before deciding the look, show a direction, build a DRAFT, look at it with preview_page, publish only when they say so), design and copy rules, and this space's BRAND RECORD (colours, fonts, logo, contact details) so you start from it. Call again with kind for what usually converts on that kind of page — local service, restaurant, appointment business, shop, product, software, opt-in, sales page, funnel, event, portfolio, link in bio. Free.
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | string | The kind of page you are building, for its playbook: local_service, restaurant, appointments, shop, product, software, lead_magnet, sales_page, funnel, event, portfolio, link_in_bio. Omit for the whole guide (and the brand). |
list_blog_authors
Scope: pages:read
List the bylines this space can use. Each is a 'member' (a Wakato person), a 'business', or 'manual' (hand-typed). Pass an id as author_id on create_blog_post so the post carries a real byline — photo, bio and links included — instead of only a name string. Member and business bylines are owned by that account and cannot be edited here.
| Parameter | Type | Required | Description |
|---|---|---|---|
include_removed | boolean | Also list bylines that were removed. Removed ones cannot be used on new posts. |
list_blog_collections
Scope: pages:read
List the space's blogs (collections). Each blog groups posts and may be attached to a site (site_id). Call before create_blog_collection to reuse an existing blog.
list_blog_posts
Scope: pages:read
List blog posts in the space. Optional collection_id, status (draft|published), limit.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection_id | string | ||
status | draft | published | ||
limit | number |
list_booking_pages
Scope: pages:read
List Space booking pages (id, name, slug, status). Reuse an existing one before creating a new one.
list_bookings
Scope: pages:read
List actual booked appointments (guest name/email, start/end, status, attendee_status = whether the guest accepted/declined the calendar invite) across the space or one booking page. Use upcoming_only:true for the forward schedule, search to find a specific guest, status:"cancelled" for cancellations. Answers in the guest's attached form (if any) are in form_data.
| Parameter | Type | Required | Description |
|---|---|---|---|
booking_page_id | string | Restrict to one booking page (omit for all pages in the space). | |
status | confirmed | cancelled | ||
upcoming_only | boolean | Only bookings that start from now, soonest first. | |
from | string | ISO datetime lower bound on start_at. | |
to | string | ISO datetime upper bound on start_at. | |
search | string | Match on guest name or email. | |
limit | number | Max rows (default 50, max 200). |
list_page_contents
Scope: pages:read
What is ON a page, without the html: every picture/video by FILE (url, asset_id when bound, how many places), every video/audio/embed with how it plays (playable: id, play, loop, sound, controls, thumbnail), the page's sections (for add_page_media), the storefront grids (tags, limit) and pinned product cards, the bound tables/documents and whether the blog index is on it. Read this before swap_page_media / set_page_products — they change what's on the page, never its layout.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes |
list_pages
Scope: pages:read
List pages in the space.
list_sites
Scope: pages:read
List the space's web artifacts (sites). Each is EITHER kind:"website" (browseable multi-page site, shared brand + platform-injected navigation) OR kind:"landing" (a landing page flow — forward-only ordered steps toward one goal, no nav; 2+ steps = a funnel). Returns id, title, slug, kind, status, root_page_id. Call this before create_site to avoid duplicates.
place_page_media
Scope: pages:write
Move a picture, video, audio or embedded video to another section or spot on the page (section = id, number or heading words from list_page_contents → sections; position start|end|before|after) and/or change its size (width: full | wide | narrow). Name it by id, url or asset_id.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
media | string | yes | |
section | string,integer | ||
position | start | end | before | after | ||
width | full | wide | narrow |
present_page
Scope: pages:read
Show a Space Page live in your chat's panel (Apps & Pages tab). Call it when you start building or changing a page, or when the user asks to open/see one. Whiteboards and documents cannot be presented. UI-only signal — succeeds even if the panel is closed, and stores nothing.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes |
preview_booking_page
Scope: pages:read
TEST a booking page the way a guest would: returns its full config, the attached form's questions (if any), and REAL bookable time slots computed with the same engine as the public /b/<slug> widget — plus a ready verdict and issues list. Each slot's label is complete — weekday, date, and time already rendered in the requested timezone (e.g. "Tuesday, July 14 at 10:30 AM"; quote verbatim — never pair a time with your own weekday/date or do timezone math) — plus an absolute start_at/end_at (UTC ISO). Use this right after create/update_booking_page to confirm it actually works. Pass a specific date (YYYY-MM-DD) to probe one day, or omit it to scan the next days (default 14) for the first open day.
| Parameter | Type | Required | Description |
|---|---|---|---|
booking_page_id | string | yes | |
date | string | Optional day to test, YYYY-MM-DD. Omit to scan forward for the next open day. | |
days | number | Days to scan when no date is given (default 14, max 60). | |
timezone | string | IANA timezone to render slot labels in (e.g. America/Los_Angeles). Defaults to the page's own timezone. |
preview_page
Scope: pages:run
SEE a page before anyone does: renders its DRAFT (or the live page) as visitors get it, saves desktop 1440px + mobile 390px screenshots to Space Assets, and runs a vision QA critique (billed to the space wallet) of real visual problems — overlap, clipping, contrast, broken images, unstyled or empty sections, mobile overflow, no clear headline or button up top. THE QUALITY LOOP: build the draft → preview_page → fix every severity=error → preview again until looks_good → show the person → publish_page only when they say so. An unpublished page is drawn as its draft, a published one as it is live. Pair with validate_site.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
viewports | string[] | Widths to capture (default both). | |
critique | boolean | Run the vision critique (default true). Set false for screenshots only. |
publish_blog_post
Scope: pages:run
Publish a blog post (live at /post/<id>), or pass publish:false to move it back to draft. Requires pages:run.
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | |
publish | boolean | false to unpublish (default true). |
publish_page
Scope: pages:run
Publish a page so it's live at /pg/<published_slug>. Pass publish:false to unpublish. Requires pages:run. The result includes an automatic link lint (link_issues): severity=error entries are DEAD links/embeds visitors will hit — fix and republish, never leave them. Publish only when the person says so — check the draft with preview_page first, and look once more at the live page afterwards.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
published_slug | string | Optional custom slug; defaults to the page's slug. | |
publish | boolean | false to unpublish (default true). |
remove_page_media
Scope: pages:write
Take a picture, video, audio or embedded video off a page — by its id (list_page_contents → playable), its url or its library file id. Media added with add_page_media goes whole (with its caption); a picture the page carries by url is removed wherever it appears. The file stays in the library.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
media | string | yes | Its id, url or asset_id. |
reorder_site_pages
Scope: pages:write
Set the display order of a site's pages (the order they appear in the site's page list and default navigation). Pass the site's page ids in the desired order; omitted pages sort after the listed ones.
| Parameter | Type | Required | Description |
|---|---|---|---|
site_id | string | yes | |
page_ids | string[] | yes | Page ids in the desired order (first = home position). |
reschedule_booking
Scope: pages:write
Move a confirmed booking to a new slot. Pass new_date (YYYY-MM-DD) + new_start (HH:MM) in the booking page's OWN timezone — find open slots with preview_booking_page first. The new slot is validated against real availability (the booking never conflicts with itself) and the linked calendar event moves with it. The guest is emailed the new time (with a calendar file that moves it) unless notify_guest:false. Returns guest_told.
| Parameter | Type | Required | Description |
|---|---|---|---|
booking_id | string | yes | |
new_date | string | yes | YYYY-MM-DD in the page's timezone. |
new_start | string | yes | HH:MM in the page's timezone. |
new_end | string | HH:MM — derived from the page's duration when omitted. | |
skip_availability_check | boolean | Force the move even if the slot isn't in the page's open availability. | |
notify_guest | boolean | Email the guest (default true), with a calendar file that updates their calendar. false = say nothing. | |
note | string | A line for the guest in that email — why, or what happens next. |
set_page_media
Scope: pages:write
Change one picture, video, audio or YouTube/Vimeo/Loom video on a page: how it plays (play: click | auto | in_view = when scrolled to; loop; sound; controls), where it starts (start_at), its volume and speed (video/audio files), its thumbnail (thumbnail_asset_id / thumbnail_url, "" to take it away), its caption, its description (alt, pictures), or its SOURCE — a new file (asset_id) or link (url) of the same kind: a new audio for an audio, a new YouTube/Vimeo/Loom link for an embed. Name it by its id (list_page_contents → playable), its url or its library file id. Works on media added with add_page_media and on what the page already had. Layout untouched — place_page_media moves or resizes.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
media | string | yes | Its id, url or asset_id (list_page_contents). |
play | click | auto | in_view | ||
loop | boolean | ||
sound | boolean | ||
controls | boolean | ||
start_at | number,string | Where playing starts: seconds (90) or a time ("1:30"). | |
volume | number | Loudness 0–100 (video/audio files; a YouTube/Vimeo/Loom player sets its own). | |
speed | number | Playback speed 0.25–2 (video/audio files). | |
thumbnail_asset_id | string | ||
thumbnail_url | string | A picture's link; "" takes the thumbnail away. | |
caption | string | ||
alt | string | What a picture shows, for people who can't see it. | |
asset_id | string | A new source: a library file of the same kind. | |
url | string | Or a new link: a file's url, or a new YouTube / Vimeo / Loom link. |
set_page_media_rules
Scope: pages:write
The page's own media rule: one_at_a_time = starting a video or audio with sound pauses any other one playing with sound (silent background loops keep playing; a YouTube/Vimeo/Loom player is another site's and isn't paused).
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
one_at_a_time | boolean | yes |
set_page_products
Scope: pages:write
Change which products a page shows, layout untouched: add = pin products beside the page's pinned cards (a card like the last one, right after it), remove = take products off THIS page (a pinned card is removed; a product that shows through a storefront grid is hidden in that grid — it stays on sale everywhere else, and add puts it back), grid = narrow a storefront grid ({index, tags: ["men"] = only products carrying all of them, [] = every product; limit: 6 or null}). Ids from list_products. On a live page the change is public at once.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
add | string[] | Product ids to pin, or to put back in a grid that hides them. | |
remove | string[] | Product ids to take off this page. | |
grid | object | Tags and/or limit for one storefront grid (index 0 = the first on the page). |
swap_page_media
Scope: pages:write
Replace one picture (or video) on a page with a library file — everywhere it appears, layout untouched, and bound to the file from then on (data-wakato-asset). Name the old one by from_url or from_asset_id as list_page_contents shows it; the new one must be a file of this space (list_document_assets, or upload_asset / add_asset_from_url first). A picture is swapped for a picture, a video for a video. On a live page the change is public at once.
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
from_url | string | The url the page carries for the picture to replace (from list_page_contents). | |
from_asset_id | string | Or the library file it is bound to. | |
to_asset_id | string | yes | The library file to show instead. |
update_blog_collection
Scope: pages:write
Edit a blog (collection): rename it, replace template_html to re-theme the post layout (keep the six tokens), or attach/detach a site_id / index_page_id (pass null to detach). Setting index_page_id writes the blog section into that page, so posts start appearing there automatically. The base_slug is fixed at creation.
| Parameter | Type | Required | Description |
|---|---|---|---|
collection_id | string | yes | |
name | string | ||
template_html | string | Replacement post layout HTML with the six tokens. | |
site_id | string,null | Attach to a site, or null to detach. | |
index_page_id | string,null | The page this blog shows on (the blog section is written into it), or null to clear. | |
card_template | string,null | HTML for ONE index card, using {{url}} {{title}} {{excerpt}} {{cover_image}} {{author}} {{date}} {{category}}. Design it however the site needs — the same markup renders every post, forever. {{cover_image}} is a complete <img class="card-cover"> or NOTHING when the post has no cover, so never wrap it in your own <img>; for a background/overlay treatment use {{cover_image_css}} (the whole background-image declaration, or nothing) rather than url('{{cover_image_url}}'), which would emit url('') on a coverless post. Match the site's own card design — cards then render bare so the host page's grid styles them. Omit for a neutral built-in card. |
update_blog_post
Scope: pages:write
Edit a blog post's title, excerpt, cover_image_url, body_html, author, or its SEO overrides (meta_title / meta_description / og_image_url — pass null to clear one and fall back to the derived value).
| Parameter | Type | Required | Description |
|---|---|---|---|
post_id | string | yes | |
title | string | ||
excerpt | string | ||
cover_image_url | string | ||
body_html | string | ||
author_id | string,null | A byline from list_blog_authors, or null to clear it. | |
author | string | ||
category | string,null | Short label shown on index cards, e.g. "Compliance". | |
meta_title | string,null | SEO <title> override (≤70 chars), or null to fall back to title. | |
meta_description | string,null | SEO meta description override (≤160 chars), or null to fall back to excerpt. | |
og_image_url | string,null | Social share image override, or null to fall back to cover_image_url. |
update_booking_page
Scope: pages:write
Edit an existing booking page — everything its settings screen has: name, length, open hours (availability), days off / special days (date_overrides), one-on-one vs group, notice, how far ahead, status, location, form, confirmation email, branding, which calendars close its slots (checks_calendars) and where bookings are also copied (copy_bookings_to). Pass booking_page_id + only the fields to change. Set meeting_location_type to "wakato_meet" so every booking auto-creates a Wakato Meet video-call link.
| Parameter | Type | Required | Description |
|---|---|---|---|
booking_page_id | string | yes | |
name | string | ||
description | string | ||
duration_minutes | number | ||
timezone | string | ||
status | draft | active | paused | ||
buffer_minutes | number | ||
min_notice_hours | number | ||
max_advance_days | number | ||
meeting_location_type | wakato_meet | in_person | phone_call | custom_link | ||
meeting_location_value | string | ||
form_id | string | Attach a Space form (id) so guests answer extra questions when booking, or null to detach. Must belong to this space. | |
availability | object[] | Weekly open hours, replacing the current ones: [{ day: 1, start: "09:00", end: "17:00" }] — day 0 = Sunday … 6 = Saturday; several ranges on one day are several rows. | |
date_overrides | object[] | Days that differ from the weekly hours (replaces the list): { date: "2026-12-24", available: false } closes a day; { date, available: true, ranges: [{ start, end }] } opens it with its own hours. | |
max_attendees | number | Group pages: how many people can book the same slot. | |
slot_interval_minutes | number,null | How often slots start (e.g. 15 = every quarter hour); null = every duration_minutes. | |
checks_calendars | string[] | Which calendars' busy events close slots on this page — keys from list_calendars ('space' = events made in this space, 'conn:<id>' = a Google/Outlook account, 'phone:<id>' = a member's phone). The others are ignored. [] = only this page's own bookings count. | |
copy_bookings_to | string,null | Also put every booking on this Google/Outlook calendar of the space (a 'conn:<id>' key from list_calendars), so it shows as taken there too. null = stop. | |
booking_type | one_on_one | group | ||
confirmation_message | string | Text shown on-page after booking AND inside the default confirmation email. | |
confirmation_email_enabled | boolean | Send the guest a confirmation email with the .ics invite (default true). false = no email. | |
confirmation_email_subject | string | Custom confirmation email subject. Merge tags: {{guest_name}} {{guest_email}} {{page_name}} {{space_name}} {{meeting_time}} {{meeting_link}} {{meeting_location}}. | |
confirmation_email_body | string | Custom confirmation email body text — replaces the default greeting/intro; the meeting time, join link, and .ics invite are always included below it. Merge tags: {{guest_name}} {{guest_email}} {{page_name}} {{space_name}} {{meeting_time}} {{meeting_link}} {{meeting_location}}. | |
branding | object | Partial branding patch, MERGED onto existing branding (null value clears a key). Keys: mode ('light'|'dark'), primary_color, background_color, card_color, text_color, muted_color, border_color, on_primary_color, input_background, subtle_color, disabled_color, font_family, radius, button_text, photo_url (https cover photo shown on the public page's brand panel), logo_url. Setting only photo_url never clobbers existing colors. |
update_page
Scope: pages:write
Update a page's title/description and/or rewrite its layout_html. To change only what's ON the page — a picture, which products show — use swap_page_media / set_page_products instead: they never touch the layout, and they are all an agent held at "Change what's on it" has. Embedded form/booking/product references are re-validated against this space. Storefront containers (data-wakato-products with optional data-tags/data-limit / data-wakato-product="<id>") and the blog index (data-wakato-blog-index) are supported the same as create_page. If the page is already published the change goes LIVE immediately — run preview_page afterwards.
DESIGN BRIEF — an unstyled or default-looking page is a FAILED build. BEFORE the first page of a conversation, call get_page_guide: it carries the space's brand record and how to work with the person — ask before deciding the look, show a direction, build a DRAFT, check it with preview_page, publish only when they say so — plus what usually converts for each kind of page (get_page_guide {kind}). The person's words, their brand and your instructions come first; never give two businesses the same page. Commit to a small system before writing HTML: one background family, one ink color, ONE accent kept for actions, AA-readable contrast; two Google Fonts via <link> (a display face with character, a plain body face); a type scale and a spacing rhythm, content in a max-width container with ≥16px side gutters. RESPONSIVE IS MANDATORY: include the viewport meta, grids/flex collapse to one column ≤768px, nothing overflows horizontally (images max-width:100%, fixed aspect via object-fit:cover), tap targets ≥44px. IMAGES: reference ONLY real URLs you know exist (space assets, generate_image results) — NEVER invent file paths; with no image available, design with gradients/patterns/typography instead of leaving broken tiles or icon placeholders. Whenever a url came with a library asset id, CARRY THE ID: <img src="<url>" data-wakato-asset="<asset_id>"> (a CSS background puts data-wakato-asset-bg="<asset_id>" on the element). That binds the media to the file, so replacing the file later updates every page using it and the owner can manage it from the page editor's media shelf. Video/audio/YouTube: add_page_media. Popups open by data-wakato-popup-open="<id>". LIVE DATA: to show a space table's rows (pricing, menu, directory, schedule), embed <div data-wakato-table="<table id>" data-columns="key1,key2" data-limit="10"></div> and let it fill itself — NEVER paste rows in as text, which freezes them the moment you write them. Only a table whose owner set web_published renders, and only its web_columns (list_tables tells you both); if the table the user means is not published, say so rather than pasting its rows in. Put a short fallback line inside the container for the case where it resolves to nothing. To show a document's text (terms, a policy, a guide, a changelog), embed <div data-wakato-document="<document id>"></div> — again never pasting the text in, and again only a document whose owner published it to the web renders (list_documents tells you which). Same for products (data-wakato-products) and blog posts (data-wakato-blog-index). Content gated behind scroll-triggered JS must still be visible with JS disabled (headless QA screenshots scroll once, nothing more).
| Parameter | Type | Required | Description |
|---|---|---|---|
page_id | string | yes | |
title | string | ||
description | string | ||
layout_html | string | Replacement HTML document. | |
meta_title | string | Search/social <title> override (≤70 chars). Defaults to the page title. | |
meta_description | string | Meta + og:description (≤160 chars). Falls back to description; when neither is set the tag is omitted, leaving any <meta name="description"> in your own layout_html to stand. | |
og_image_url | string | Social share image for this page. |
update_site
Scope: pages:write
Update a web artifact after creation: title, description, nav_layout (top/sidebar/none), logo_url, root_page_id (the home page / step 1), and the managed nav_config. nav_config replaces the whole nav when provided: { enabled, items: [{label?, page_id? | url?}], cta: {label, page_id? | url?}, style: {sticky?, theme? (auto/light/dark), transparent_on_hero?, logo_position? (left/center), accent_color?} }. Page items must be pages of THIS site; item labels default to the live page title. WEBSITES ONLY for nav: landing pages are forward-only and any attempt to enable a nav on kind:landing is rejected — if visitors should browse freely, build a website instead.
| Parameter | Type | Required | Description |
|---|---|---|---|
site_id | string | yes | |
title | string | ||
description | string | ||
nav_layout | top | sidebar | none | ||
logo_url | string | Brand logo URL. Empty string removes the logo. | |
root_page_id | string | Page id to use as the site's home page. | |
nav_config | object | Managed navigation. items = ordered nav entries ({page_id} for site pages, {url} for external/public links like /f/<slug>); cta = optional highlighted button; style = bar appearance. |
validate_site
Scope: pages:read
Readiness lint for a site BEFORE (or after) publishing: reports dead links (/pg/, /f/, /b/, /pay/, /post/ that resolve to nothing), nav items pointing at deleted or draft pages, form/booking/product embeds that don't exist in this space, and unpublished pages. Run this after building a multi-page site and FIX every error it reports — a site with dead links is not done. Returns { ok, pages_total, pages_published, issues: [{severity, code, message, page_id?, detail?}] }.
| Parameter | Type | Required | Description |
|---|---|---|---|
site_id | string | yes |