Blueprints
A blueprint describes what a marketplace should look like. The agent writes one,
previews it with apply_blueprint and dry_run: true, reads
the plan, then applies it with the plan_hash the preview returned.
{ "settings": { "App.name": "Acme", "Seo.title": "Hébergements insolites partout en France" }, "user_groups": [ { "title": "Hôtes", "handle": "hotes", "can_post_listing": true }, { "title": "Voyageurs", "handle": "voyageurs", "can_post_listing": false } ], "listing_attributes": [ { "title": "Type de bien", "alias": "type_de_bien", "input_type": "select", "searchable": true, "search_filter": true, "options": ["Cabane", "Yourte", "Roulotte"] }, { "title": "Capacité", "alias": "capacite", "input_type": "number" } ], "categories": [ { "title": "Logements insolites", "slug": "logements-insolites", "children": [ { "title": "Cabane dans les arbres" }, { "title": "Yourte", "children": [{ "title": "Yourte avec spa" }] } ]} ], "order_types": [ { "handle": "reservation", "title": "Réservation", "transaction_type": "booking", "booking_type": "period", "calendar_enabled": true, "price_enabled": true, "supplier_fees_percentage": 12 } ]}How a blueprint is read
Section titled “How a blueprint is read”Sections are applied in a fixed order, whatever order you write them in:
settings, user_groups, listing_attributes, categories, order_types,
collection_models, collections, pages, blog, menus. Each section is planned
against the marketplace as it stands before the apply.
Each entry is identified by its key — a slug, a handle or an alias, depending on the
section. An entry whose key exists updates that item; one whose key does not exist creates a
new item. When you omit the key, most sections derive it from the title, so the same
blueprint gives the same key on every run. To update something, copy its key from
describe_marketplace.
Absence is never deletion. An item missing from the blueprint is left as it is. You
delete in two ways, both requiring the marketplace:destroy scope:
"delete": trueon one entry — supported bypages,collectionsandcollection_models;prune: trueon the call — deletes, in every section the blueprint carries, the items it leaves out.settingsandmenusare never pruned.
The server declines deletions that would lose data (a category with listings, a group with
members…). They appear in the plan as noop operations with a note explaining why.
Unknown names are refused, not ignored. An unknown section, or an unknown field in an entry, stops the whole blueprint with a message listing what is accepted. A blueprint never half-applies because of a typo. (One exception: an unknown key inside a listing attribute’s option is ignored.)
Send JSON types. Booleans as true/false and numbers as numbers. Most fields read a
string such as "false" as true; a few refuse anything but a boolean (a menu link’s
new_tab, the no_index and no_follow of collections and models).
References by id need two applies. Collection conditions name categories, groups,
order types and attribute options by the ids describe_marketplace gives. Something created
by a blueprint gets its id when it is applied, so a condition naming it goes in the next
blueprint. References by slug (a page’s parent, an article’s category) can point to an
item created in the same blueprint.
settings
Section titled “settings”settings:write. An object of setting name to value. Values are strings, numbers, booleans
or null, stored as text: true as 1, false and null as an empty value.
{ "settings": { "App.name": "Acme", "App.lang": "fr", "Seo.title": "Hébergements insolites partout en France", "Theme.footer.nb_columns": "3", "Theme.Blog.grid_preset": "preset-2" }}Writable names
| Name | Values |
|---|---|
App.name, App.lang, App.timezone |
Exactly these three names |
Any name starting with Homepage., Listing., Blog., Contact. or Seo. |
Text. Use names describe_marketplace shows. |
Theme.footer.nb_columns |
0 to 4 |
Theme.footer.columns.1.content to Theme.footer.columns.4.content |
HTML (see Markup) |
Theme.footer.social_buttons |
1 or true switches them on; any other value switches them off |
Theme.Listing.grid_preset, Theme.User.grid_preset |
preset-1, preset-2 |
Theme.Blog.grid_preset |
preset-1, preset-2, preset-3 |
ThemeSettings.<package>.<option> |
An option of the active theme package, as listed in describe_marketplace → theme.package.options[].key, validated by its type; a select takes one of its choices. Writing an option’s default value removes the stored value. |
Everything else is refused, Theme.* included: the theme, its custom CSS and JavaScript and
its colour file are set by an administrator in the back office (Themes → Customize).
A name or a value that looks like a credential is refused. Settings are never deleted, even
with prune.
user_groups
Section titled “user_groups”settings:write. A list of member groups, keyed on handle.
{ "user_groups": [{ "title": "Hôtes", "handle": "hotes", "can_post_listing": true, "requires_moderation": true }] }| Field | Type | Notes |
|---|---|---|
title |
string | Required in every entry |
handle |
string | The key. Derived from title when omitted (Garage partenaire → garage-partenaire). A new title keeps it: unlike in the back office, the address does not follow the title. |
can_post_listing |
boolean | Members may post listings |
is_onboarding |
boolean | |
is_active |
boolean | |
is_public |
boolean | |
can_change_group |
boolean | |
requires_moderation |
boolean | |
sort_priority |
integer | |
type |
integer | 1 individual, 2 organisation, 3 non-profit |
Renaming a group means sending its existing handle with the new title. A group without a
handle (describe_marketplace shows handle: null) cannot be written by a blueprint.
Deleting: prune only. A group with members is never deleted.
listing_attributes
Section titled “listing_attributes”catalog:write. A list of the fields a listing carries, keyed on alias, each with its
options.
{ "listing_attributes": [ { "title": "Équipements", "alias": "equipements", "input_type": "checkbox", "options": ["Wi-Fi", "Spa", { "title": "Animaux acceptés", "alias": "animaux" }] } ]}| Field | Type | Notes |
|---|---|---|
title |
string | Required in every entry |
alias |
string | The key. Derived from title when omitted, with underscores (Type de bien → type_de_bien). |
input_type |
string | select, checkbox, input, textarea, number, boolean, link, price, date, text. Defaults to select, also on an update: always send it. |
value_type |
string | string, text, number, json |
description |
string | |
required |
boolean | |
searchable |
boolean | |
search_filter |
boolean | Offered as a search filter |
display_unselected_option |
boolean | |
apply_to_all |
boolean | Attached to every category |
visibility |
integer | 1 everyone, 2 administrators only. Other numbers are not refused: send one of these. |
sort_priority |
integer | |
options |
list | Required for select and checkbox (at least one), refused for other types |
An option is a string (its title) or an object { "title", "alias"?, "sort_priority"? }.
Options are keyed on their alias, derived from the title when omitted; sort_priority
defaults to the option’s position in the list. Options you leave out are kept, unless you
prune.
Deleting: prune only. It deletes the attributes the blueprint leaves out, and the
options it leaves out of the attributes it carries. Values already recorded on listings for a
deleted attribute are lost. System attributes are never deleted.
categories
Section titled “categories”catalog:write. The category tree, nested with children, keyed on slug.
{ "categories": [ { "title": "Logements insolites", "slug": "logements-insolites", "children": [ { "title": "Cabane dans les arbres", "slug": "cabane-dans-les-arbres", "is_featured": true } ]} ]}| Field | Type | Notes |
|---|---|---|
title |
string | Required in every entry |
slug |
string | The key, and the category’s public address. Derived from title when omitted. A new title keeps it: unlike in the back office, the address does not follow the title. |
children |
list | Subcategories. Three levels at most. |
page_title, description, description_long, meta_title, meta_description, image_alt |
string | |
display_in_search, display_in_menu, is_filter, is_featured, cart_ok |
boolean | |
sitemap_changefreq |
string | always, hourly, daily, weekly, monthly, yearly, never |
sitemap_priority |
number | 0 to 1 |
Three rules matter:
- The slug is the identity. Two categories may share a title, never a slug. To change a
category, carry its slug: a renamed category without its slug is a new category, and the
plan says so in a note (
A category titled "Diagnostic" already exists under the slug "diagnostic-auto". This will create a SECOND one…). A slug cannot be changed here; changing the title keeps the slug and the address. - The tree comes from the nesting. An entry’s parent is the entry it is nested in, and a
top-level entry has no parent. To update a subcategory, nest it under its parent, as in
the tree. Placing an existing category elsewhere moves it, with its subcategories (the
plan notes it).
parent_id,lft,rght,leveland similar fields are refused. - Three levels at most. A fourth level is refused.
Deleting: prune only. A category that has subcategories or holds listings is never
deleted; a branch you want gone takes one apply per level.
order_types
Section titled “order_types”catalog:write. What ordering means on the marketplace: selling, booking, digital or
display only.
{ "order_types": [ { "handle": "location-courte", "title": "Location courte durée", "transaction_type": "booking", "booking_type": "period", "calendar_enabled": true, "price_enabled": true, "customer_fees_percentage": 5, "supplier_fees_percentage": 10 } ]}Key. Order types have no slug. Give each one a handle you choose: it becomes the
type’s URL token when the type is created, and matches it afterwards. Without a handle,
types are matched on their exact title, and a rename creates a new type. The URL token of
an existing type is never changed; to update a type created in the back office, send its
url_token (from describe_marketplace) as handle, or its exact title.
| Field | Type | Notes |
|---|---|---|
title |
string | Required in every entry |
handle |
string | The key (see above) |
transaction_type |
string | no_transaction, booking, selling, digital_asset |
booking_type |
string | period, qty |
order_btn_text, availability_type |
string | |
apply_to_all_groups, price_enabled, shipping_enabled, weight_enabled, stock_enabled, stock_auto_decrement, restock_on_cancel, hide_if_out_of_stock, min_qty_enabled, qty_in_cart, calendar_enabled, last_day_included, order_time_enabled, digital_assets_enabled, listings_needs_approval, vendor_manage_publish_status, all_new_listings_on_homepage, use_supplier_avatar_as_listing_cover, contact_supplier_enabled, supplier_fixed_fees_on_item, customer_fixed_fees_on_item |
boolean | |
supplier_fees_flat, customer_fees_flat |
number | Not negative |
supplier_fees_percentage, customer_fees_percentage |
number | 0 to 100 |
min_image_number, max_image_number |
integer | Not negative; min not above max |
upload_listing_image_mandatory |
boolean | true sets min_image_number to 1 when you do not send min_image_number |
A new type starts from the platform’s defaults; the blueprint overrides what it names.
Deleting: prune only. A type used by listings is never deleted, and a type the
blueprint names (by handle or by title) is kept. Check the plan’s delete operations
before applying a prune all the same: a type you renamed without a handle is a new type,
and the old one is pruned.
collection_models
Section titled “collection_models”catalog:write. A collection model makes one storefront collection per value of a
criterion (one per brand, per city…) and keeps them in step. Keyed on handle, an internal
name: a model has no page of its own.
The fields and their rules are those of the Admin API (create a collection model).
{ "collection_models": [ { "handle": "logements-par-type", "title": "Logements par type", "type": "listing", "pivot": { "type": "attribute", "field": "type_de_bien", "values": [ { "value": "811", "label": "Cabane" }, { "value": "812", "label": "Yourte" } ]}, "handle_pattern": "logements-{value}", "title_pattern": "Logements : {value}", "sort_by": "manual", "collections": { "812": { "page_title": "Dormir dans une yourte" } } } ]}| Field | Notes |
|---|---|
handle |
Required. The key. |
title, pivot, handle_pattern |
Required to create a model |
type |
listing (default) or user. Fixed once the model has collections: make a new model instead. |
pivot |
{ "type", "field"?, "values": [{ "value", "label" }] }. type: attribute, category, seller_group, order_type, city, country, department or tag (for users: seller_group, city, country, department); field: an attribute’s alias. Sent, it replaces the previous criterion whole. |
rules |
Conditions every collection of the model shares (see Conditions), all combined with AND |
sort_by |
alphabetical, count, manual |
handle_pattern, title_pattern, page_title, subtitle, description |
Patterns: {value} stands for each value’s label |
image_alt, template, thumbnail, meta_title, meta_description, meta_keywords, no_index, no_follow, sitemap_changefreq, sitemap_priority |
Defaults for its collections |
collections |
{ "<value>": { … } }: the texts one of its collections replaces. null takes the model’s pattern again; "" leaves the field empty on purpose, except title (a collection always has a name). Works for a collection the same blueprint creates. |
detach |
["<value>", …]: values you took out of pivot.values whose collection leaves the model and keeps what it has, instead of going offline |
delete |
true, alone with handle, deletes the model |
The plan shows what happens to each of its collections, under the collections section:
- a new value creates a collection, online, at the address its pattern gives — or at the
first free
-2,-3… when that address is taken, and the note says by what; - the conditions of the others follow the model;
- a value taken out puts its collection offline, its texts kept;
- a value that comes back puts its collection back online.
Deleting a model leaves its collections online, on their own. Images are uploaded in the
back office: image is refused.
collections
Section titled “collections”catalog:write. Storefront collections, keyed on handle, which is their address
(/handle). Another handle is another collection.
The fields and their rules are those of the Admin API (create a collection).
{ "collections": [ { "handle": "cabanes-avec-spa", "title": "Cabanes avec spa", "page_title": "Cabanes avec spa privatif", "is_online": true, "rules": { "inclusion": { "type": "AND", "rules": [ { "type": "category", "operator": "is", "value": ["102"] }, { "type": "attribute", "field": "equipements", "operator": "is", "value": ["905"] } ]}, "exclusion": [] } }, { "handle": "promo-ete", "delete": true, "redirect_to": "/cabanes-avec-spa" } ]}| Field | Notes |
|---|---|
handle |
Required. The key and the address. |
title |
Required to create a collection |
storefront_enabled |
A new collection is a storefront page (true) |
is_online |
A new collection is offline unless you set true |
type |
listing or user |
rules |
Its conditions (see below) |
page_title, subtitle, description (HTML), image_alt, canonical_url, meta_title, meta_description, meta_keywords |
|
template |
A collection template suffix the theme has |
thumbnail |
A card snippet the theme has |
no_index, no_follow |
Booleans. A new collection is not indexed by search engines unless you send no_index: false |
sitemap_changefreq, sitemap_priority |
|
delete |
true, alone with handle and optionally redirect_to |
redirect_to |
With delete only: an address of this marketplace (/another-address) that the deleted address redirects to permanently (301). Without it, the address answers 404. |
A collection’s address cannot be one a page already holds, or one the marketplace reserves.
A collection made by a model takes its conditions, type and online state from the
model. Here you can only replace its texts: title, page_title, subtitle,
description, image_alt, template, thumbnail, canonical_url, meta_title,
meta_description, meta_keywords, no_index, no_follow, sitemap_changefreq,
sitemap_priority — null for the model’s, "" for nothing (not for title: a collection
always has a name). Writing the same collection
here and under its model in one blueprint is refused (written twice).
Deleting: delete: true on the entry, or prune (which deletes the storefront
collections without a model that the blueprint leaves out). Neither reaches a collection made
by a model (it leaves through its model) or a collection without a storefront page.
Conditions
Section titled “Conditions”{ "inclusion": { "type": "AND", "rules": [{ "type": "…", "operator": "…", "value": "…", "value_2": "…", "field": "…" }] }, "exclusion": [{ "type": "…", "operator": "…", "value": "…" }]}A collection presents the items matching its inclusion conditions — all of them (AND) or
any one (OR) — minus those matching one of its exclusions. Empty conditions (no inclusion
rule and no exclusion) are stored as no conditions.
type |
What it compares |
|---|---|
title |
The listing’s title |
category |
Category ids (subcategories included) |
vendor |
Seller user ids |
seller_group |
User group ids |
order_type |
Order type ids |
shipping_method |
Shipping method ids |
attribute |
An attribute (field: its alias) and option ids, or numbers |
tag, city, country |
Text values |
department |
Department codes |
price, original_price, weight, stock |
Numbers |
location_radius |
A distance around a point |
Operators: is, not, less, more, between (with value_2), starts_with,
ends_with, contains, not_contains, is_set, is_not_set, within. Exclusions accept
the types that name what an item belongs to: category, tag, vendor, seller_group,
order_type, shipping_method, city, country, department.
The simplest way to get conditions right is to read an existing collection with
read_item and start from its rules.
content:write. CMS pages, keyed on slug, the page’s address.
{ "pages": [ { "slug": "a-propos", "title": "Qui sommes-nous ?", "body": "<p>Acme rassemble les hébergements insolites de France.</p>", "is_online": true }, { "slug": "equipe", "title": "L'équipe", "parent": "a-propos" }, { "slug": "faq", "is_online": false }, { "slug": "ancienne-offre", "delete": true, "redirect_to": "/a-propos" } ]}| Field | Type | Notes |
|---|---|---|
slug |
string | The key. Derived from title when omitted. |
title |
string | Required to create a page; optional to update one |
parent |
string | Slug of the parent page, which may be created in the same blueprint. Omit it to leave the page where it is; a page cannot be moved back to the top level here. A parent that does not exist is reported at apply, not in the preview. |
body |
string | HTML (see Markup) |
page_title, subtitle, template, layout, identifier, locale, meta_title, meta_description, meta_keywords, canonical_url |
string | |
is_online, no_index, no_follow, is_searchable |
boolean | |
sort_priority |
integer | |
delete |
true |
Alone with slug and optionally redirect_to |
redirect_to |
string | With delete only, as for collections |
An update carries only what changes. The slug cannot be changed: another slug is another page. Custom CSS and JavaScript are not writable on a page.
Deleting: delete: true on the entry, or prune. A page that has child pages is never
deleted.
content:write. Blog categories and articles, both keyed on slug.
{ "blog": { "categories": [{ "title": "Actualités", "slug": "actualites" }], "posts": [ { "title": "Bienvenue sur Acme", "category": "actualites", "summary": "Ce que vous trouverez ici.", "body": "<p>…</p>", "cover": "aurora", "image_alt": "Une cabane au lever du jour", "is_online": true } ] }}The section accepts posts or articles for the article list, and articles may also be
nested in their category ("categories": [{ "title": …, "posts": [ … ] }]). A plain list is
read as categories only.
Categories
| Field | Type | Notes |
|---|---|---|
title |
string | Required in every entry |
slug |
string | The key. Derived from title when omitted. A new title keeps it: unlike in the back office, the address does not follow the title. |
description, page_title, locale, meta_title, meta_description, meta_keywords, canonical_url |
string | |
no_index, no_follow |
boolean |
Articles
| Field | Type | Notes |
|---|---|---|
title |
string | Required in every entry |
slug |
string | The key. Derived from title when omitted. A new title keeps it: unlike in the back office, the address does not follow the title. |
category |
string | Required. Slug of a blog category, existing or created in the same blueprint. Used when the article is created. A category that does not exist is reported at apply, not in the preview. |
summary, body (HTML), image_alt, meta_title, meta_description |
string | |
is_online |
boolean | false when omitted, also on an update. Send true to keep a published article online. |
no_index, no_follow |
boolean | |
cover |
string | One of the built-in covers aurora, sienna, moss, plum, slate, amber, or an https:// image URL |
An article’s publication date is the day it is created, and cannot be set.
Deleting: prune only. It deletes the blog categories the blueprint leaves out (never
one that holds articles) and every article it leaves out.
content:write. The links of existing menus, keyed on the menu’s slug (main-menu,
footer-menu… as describe_marketplace lists them).
{ "menus": [ { "menu": "footer-menu", "links": [ { "title": "Qui sommes-nous ?", "url": "/a-propos" }, { "title": "Nous écrire", "url": "mailto:contact@example.com" }, { "title": "Instagram", "url": "https://instagram.com/acme", "new_tab": true } ]} ]}An entry gives all the links of the menu, in order, and replaces them; [] empties the
menu. A link left out is removed, which is an edit, not a deletion: it does not need
marketplace:destroy, and what it pointed to is untouched.
| Field | Notes |
|---|---|
menu |
Required. Slug of an existing menu. Menus are not created or deleted here. |
links |
Required. At most 50. |
links[].title |
Required. Plain text, 255 characters at most, no < or > |
links[].url |
Required. An address of the marketplace (/contact), http:// or https://, mailto: or tel:. 255 characters at most. |
links[].new_tab |
true or false (default) |
Markup
Section titled “Markup”Page and article bodies, collection and model descriptions, footer columns and several homepage settings are HTML shown as is on the storefront. Anywhere in a blueprint, a string whose markup would run code is refused, with its path. Nothing is cleaned silently: what would be stored differently from what you wrote is refused.
- Allowed elements:
p,br,hr,span,div,section,article,header,footer,aside,h1–h6,strong,b,em,i,u,s,small,sub,sup,mark,abbr,cite,q,code,pre,kbd,del,ins,time,blockquote,address,figure,figcaption,ul,ol,li,dl,dt,dd,table,caption,thead,tbody,tfoot,tr,th,td,colgroup,col,a,img,picture,source. Any other element is refused. - Refused attributes: any
on…event attribute,srcdoc,formaction,xlink:href; ajavascript:,vbscript:ordata:URL inhref,src,srcsetand other URL attributes; astyleusingexpression(,behavior:or-moz-binding. A value that is itself ajavascript:,vbscript:ordata:URL is refused wherever it appears (acanonical_url, a setting). - Settings: a value without any element that holds a quote followed by
name=is refused, because some settings are printed inside an HTML attribute.
pages[0].body: contains a <script> element. Not writable through MCP: markup that runs code is added by the marketplace admin, by hand, in the back-office editor of this item, not by an agent. Write it without, or tell the admin to add it there.Validation errors
Section titled “Validation errors”A refused blueprint comes back as a tool error (isError: true) whose text starts with the
path of the problem. Nothing is written. The most frequent ones:
| Message | Cause |
|---|---|
<section>: unknown section. Supported: … |
The section does not exist (a theme section, for example) |
<path>: unknown field(s): …. Writable: … |
A field the section does not write |
<path>.title: is required. |
Missing title where every entry needs one |
<path>.slug: duplicate slug "…". |
Two entries resolve to the same key; give one an explicit slug |
listing_attributes[0].options: an attribute of type "select" needs at least one option. |
select or checkbox without options (remember input_type defaults to select) |
categories[0].children[0].children[0].children: categories may be nested 3 levels deep at most; … |
A fourth level |
order_types[0].supplier_fees_percentage: cannot exceed 100. |
Out-of-range commission |
settings.Theme.custom_js: not writable through MCP: … |
A theme setting outside the writable list; the message names the back-office screen |
settings.Stripe.mode: this key is not writable through MCP. … |
A setting outside the writable names |
settings.<name>: the value looks like a …; credentials do not belong in settings. |
A credential-shaped value |
pages[faq].title: is required to create a page; no page has this slug yet. |
A new page without a title (often a mistyped slug) |
pages[<slug>].delete: 2 child page(s) sit under it. Delete or move them first. |
Deleting a page that has children |
collections[<handle>].redirect_to: only goes with delete: true. |
redirect_to on an entry that is not deleted |
collections[<handle>]: … written on the model "…" this collection is made by … |
Conditions, type or online state on a model’s collection |
menus[<slug>]: no such menu. Menus: main-menu, footer-menu. Menus are not created here. |
Unknown menu |
blog.posts[0].category: is required: name the slug of the category this article belongs to. |
Article without a category |
Errors from the collection and model rules (a held address, an unknown template, a missing pattern…) come back in the same format, with the rule’s own message.