Sites and settings
Creating a network site as a clone of a donor, reading a site card, and the site-level settings that used to need wp-admin.
These endpoints create a network site as a clone of a donor, optionally emptied down to its design, and manage what used to need the WordPress admin: title and tagline, search engine visibility, site icon, analytics counters, the static front page and caches.
The contract lives in the Node service; the work is done by a PHP executor. Creating a site is a network operation and runs on the main site. Everything under /site/* runs on the target site’s own domain, because option writes trigger WordPress hooks — update_blog_public, Polylang, caches — that do not fire under switch_to_blog().
| Endpoint | Who may call it |
|---|---|
POST /sites | network super admin only |
GET /sites/{id} | anyone with access to that site |
/site/* | an administrator of the site, or a super admin; an editor or author is refused with 403 forbidden |
POST /sites
| Field | Type | Description |
|---|---|---|
name | string | subdomain: lowercase latin letters, digits and hyphens, up to 50 characters. www, api, admin, mail, ftp, cdn, static, app, apps, stage, test are reserved. The site gets <name> under the network domain |
title | string | site title; name by default |
donor | integer | blog_id of the site to clone |
empty | boolean | purge the content of the new site after the clone; false by default |
keep_frames | array | frame ids or slugs that empty keeps; only together with empty: true |
copy_files | boolean | copy the donor’s uploads; true by default. With empty: true nothing points at those files, so pass false |
public | boolean | false sets blog_public and blogs.public to 0 at once — noindex from the first request. Omitted — as the donor, and the response says public: null |
What empty removes, in the new site’s tables only — every statement checks that the table carries the new site’s prefix:
- every post of every type — pages, posts, attachments, menu items, revisions, service types — with their meta, term relations and comments;
- Polylang translation groups and navigation menus, with the menu locations in the theme and in Polylang;
- apps and datasets;
- frames not listed in
keep_frames: registry entries, sources and baked copies; the bake queue is emptied; - the Polylang language list and translated slugs transients, and critical CSS transients;
- Yoast indexables, all
yoast_*tables except migrations; - the donor’s analytics: counters zeroed, index data and view counts removed, the domain wizard state and the app proxy reset;
- options that pointed at removed posts: the front page is reset to posts,
page_on_front,page_for_posts,site_iconandsite_logobecome 0.
What stays: the theme, Polylang languages and settings, custom CSS and design tokens, users and roles, and the kept frames.
curl -s -X POST "$BASE/sites" \
-H "Authorization: Bearer $OP_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "example",
"title": "Example",
"donor": 233,
"empty": true,
"keep_frames": ["site__header", "site__footer", "article"],
"copy_files": false,
"public": false
}'
The answer is 201:
{
"success": true,
"blog_id": 241,
"domain": "example.onpress.pro",
"donor": 233,
"public": false,
"files": "files not copied (copy_files=false)",
"purged": {
"pages": 21,
"menus": 6,
"datasets": 4,
"frames": 20,
"apps": 2,
"transients": 2,
"posts_total": 58,
"post_types": { "page": 21, "nav_menu_item": 30, "revision": 7 },
"translation_groups": 5,
"frames_kept": { "cb7ccee58078d1fb": "site__header", "1dc0afb7671182c6": "site__footer", "91399e3637b7aa2b": "article" },
"frames_missing": [],
"yoast_tables": ["yoast_indexable", "yoast_indexable_hierarchy", "yoast_primary_term", "yoast_seo_links"]
}
}
purged is null without empty. frames_missing lists keep_frames entries the donor does not have. The kept frames still hold copies baked on the donor, with its menus and texts: rebake them once the new site has its own menus and data, see How it works.
The clone runs synchronously and takes tens of seconds; the service waits up to five minutes (SITE_CREATE_TIMEOUT_MS). A repeated call with the same name is safe: it answers 409 site_exists with the existing blog_id.
| Error | HTTP | When |
|---|---|---|
forbidden | 403 | the token’s owner is not a network super admin |
invalid_name | 400 | the name breaks the rules above or is reserved |
invalid_donor | 400 | donor is not a positive integer |
invalid_keep_frames | 400 | not an array of strings, or given without empty: true |
donor_not_found | 404 | no such site in the network |
site_exists | 409 | the domain is taken; blog_id and domain in the answer |
purge_failed | 500 | the clone exists (blog_id in the answer) but the purge stopped; nothing outside the new site was touched |
clone_failed | 500 | the cloner failed for another reason |
internal_unavailable | 502 | the PHP executor did not answer in time or is not deployed |
GET /sites/{id}
A site card: domain, visibility, theme, front page, languages and how much is in it. Access is checked twice — to the site chosen for the request and to {id} — so unless you are a super admin, send X-OnPress-Blog-Id with the same id.
curl -s "$BASE/sites/235" \
-H "Authorization: Bearer $OP_TOKEN" \
-H "X-OnPress-Blog-Id: 235"
{
"success": true,
"blog_id": 235,
"domain": "example.onpress.pro",
"path": "/",
"public": 0,
"blog_public": 0,
"archived": 0,
"deleted": 0,
"registered": "2026-09-21T19:26:03.000Z",
"last_updated": "2026-09-21T20:38:14.000Z",
"blogname": "Pomelli",
"blogdescription": "Free AI Marketing Tool by Google",
"home": "https://example.onpress.pro",
"theme": { "template": "onpress-v2", "stylesheet": "onpress-v2", "current_theme": "onpress-v2" },
"show_on_front": "page",
"page_on_front": 6409,
"site_icon": 6425,
"languages": { "default": "en", "list": ["en", "ru"] },
"counts": {
"post_types": { "attachment": 73, "nav_menu_item": 23, "page": 24 },
"pages": 24,
"menus": 6,
"frames": 6,
"datasets": 6
}
}
public is the network flag in blogs, blog_public the site option; both 0 mean the site asks not to be indexed. languages is null on a site without Polylang. counts.post_types leaves out auto-drafts and trash. There is no frame_defaults field: frames have no defaults any more.
Errors: 400 invalid_blog_id, 404 site_not_found, 403 forbidden_site.
GET /site/settings, PATCH /site/settings
The site is chosen as everywhere: blog_id in the body or query, X-OnPress-Blog-Id, or domain.
| Field | Type | Description |
|---|---|---|
blogname | string | site title, not empty |
blogdescription | string | tagline |
blog_public | 0, 1 or boolean | search engine visibility; written to the blog_public option and to blogs.public |
site_icon | integer | attachment id of an image on this site; 0 removes the icon |
analytics | object | keys of onpress_analytics to set; keys not passed stay, unknown keys are rejected |
Unknown top-level fields are rejected with unknown_field. A write that changes something flushes the site’s page cache and object cache; the answer says what was flushed in cache.
curl -s "$BASE/site/settings?blog_id=235" -H "Authorization: Bearer $OP_TOKEN"
{
"success": true,
"blog_id": 235,
"settings": {
"blogname": "Pomelli",
"blogdescription": "Free AI Marketing Tool by Google",
"blog_public": 0,
"blogs_public": 0,
"site_icon": 6425,
"site_icon_url": "https://example.onpress.pro/wp-content/uploads/sites/235/2026/09/cropped-993838e9_1771708768.png",
"analytics": {
"ym_counter_id": "",
"ym_enabled": 0,
"ym_counter_name": "",
"ywm_client_token": "",
"ywm_client_verified": 0,
"gsc_verification_token": "",
"gsc_site_url": "",
"gsc_verified": 0,
"ga_property_id": "",
"ga_measurement_id": "",
"ga_property_name": "",
"ga_enabled": 0,
"…": "…"
},
"domain": "example.onpress.pro",
"template": "onpress-v2",
"stylesheet": "onpress-v2",
"show_on_front": "page",
"page_on_front": 6409
}
}
PATCH takes the same fields and answers with changed — the names of the fields that actually changed — the new settings and cache:
curl -s -X PATCH "$BASE/site/settings" \
-H "Authorization: Bearer $OP_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"blog_id": 241, "blogname": "Example", "blogdescription": "Guide to Example", "blog_public": 0}'
{
"success": true,
"blog_id": 241,
"changed": ["blogdescription"],
"settings": { "blogname": "Example", "blogdescription": "Guide to Example", "blog_public": 0, "…": "…" },
"cache": { "object": true, "supercache": { "status": "flushed", "dir": "…/cache/supercache/example.onpress.pro" } }
}
Errors, all 400: unknown_field, nothing_to_change (no known field passed), invalid_blogname, invalid_blogdescription, invalid_site_icon, invalid_analytics. Not an administrator of the site — 403 forbidden.
PUT /site/front-page
| Field | Type | Description |
|---|---|---|
page | integer or string | page id or path |
lang | string | when a path is shared by several languages, take the page of this language |
Sets show_on_front to page and page_on_front. With Polylang the option always holds the page of the default language: pass any page of a translation group and its default-language member is written, and Polylang serves the other members as the front pages of their languages. The page must be published. Polylang’s language cache, the page cache and the object cache are flushed.
{
"success": true,
"blog_id": 241,
"changed": true,
"show_on_front": "page",
"page_on_front": 12,
"front_pages": {
"en": { "id": 12, "url": "https://example.onpress.pro/" },
"ru": { "id": 14, "url": "https://example.onpress.pro/ru/" }
},
"before": { "show_on_front": "posts", "page_on_front": 0 }
}
| Error | HTTP | When |
|---|---|---|
invalid_page | 400 | page is neither a positive id nor a non-empty path |
invalid_lang | 400 | lang is not a language slug |
page_not_found | 404 | no such page |
page_not_published | 409 | the page is a draft or pending |
no_default_translation | 409 | the page is not in the default language and has no default-language translation; link the translations first |
A home page moves between translation groups only through this call: publishing it with translation_of is refused with front_page_group_conflict, see Pages and markdown.
POST /site/cache/flush
| Field | Type | Description |
|---|---|---|
transients | array | transient names to delete; a trailing * deletes every transient with that prefix |
supercache | boolean | remove the site’s WP Super Cache directory |
object | boolean | flush the object cache |
polylang | boolean | drop Polylang’s language list and translated slugs caches |
At least one is required, otherwise 400 nothing_to_flush; a malformed name is 400 invalid_transients.
curl -s -X POST "$BASE/site/cache/flush" \
-H "Authorization: Bearer $OP_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"blog_id": 241, "transients": ["pll_languages_list", "onpress_critical_*"], "object": true, "supercache": true}'
{
"success": true,
"blog_id": 241,
"flushed": {
"transients": { "deleted": ["pll_languages_list", "onpress_critical_singular"], "missing": [] },
"object": true,
"supercache": { "status": "empty", "dir": "…/cache/supercache/example.onpress.pro" }
}
}