weaverse-content-api — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited weaverse-content-api (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Read and edit Weaverse content (projects, pages, theme settings, translations) outside the Studio editor, over an authenticated REST API. Use it for bulk edits, AI/automation content pipelines, staged draft→publish flows, and scheduled or versioned content changes.
https://studio.weaverse.io/api/v1/contentGET https://studio.weaverse.io/api/v1/content/openapi.json (OpenAPI 3.1, no auth) — the authoritative contract. Fetch it for exact field-level schemas; this skill is the workflow guide.references/endpoints.md. Rich-text/Portable Text details: references/portable-text.md.scripts/weaverse_content_api.mjs (zero-dependency, Node 18+).Not every write goes live. The API has three write models:
PATCH .../pages/:type/*handle (item data), PATCH .../theme-settings, and translation writes apply immediately and invalidate caches, going live through api.weaverse.io — the same path a Studio save takes.GET/PUT/DELETE .../page-drafts/:pageId), global-section drafts (op=save-draft/discard-draft), and pending-version replacement (PUT /versions/{versionId}, CAS-fenced; drafts refused) don't change live by themselves. Promotion is a live write: POST /publish-versions/{versionId} (publish a saved draft), POST /versions/{versionId}/publish-now (publish a scheduled version immediately), or POST /versions/{versionId}/restore (immediately overwrites live content — a confirm-gated rollback). POST /versions/{versionId}/schedule is not inert either: it commits an automatic live change at publishAt with no further call — so a PUT replacement of an already-scheduled version also goes live at that time. All of these need content:publish (restore also content:delete).POST /projects with op=create|copy|delete|set-settings|purge-cache (scope project:manage; delete also needs content:delete + confirm). Projects can be created through the API — a blank or theme-seeded project, or a background copy of this shop's own or a public demo-store project.So the lifecycle is:
Create project → POST /projects (op=create) — or start from Weaverse Builder / demo store
Create initial pages → import a project JSON into Studio (generating-weaverse-project-json)
OR create pages one by one via POST /projects/:projectId/pages
Update content → live PATCH (item edits, theme settings) when immediacy is wanted
OR draft → review → publish / schedule when it must be gated
Fix a bad publish → restore a version (confirm-gated) or re-PATCHPATCH with a type on the new id) or relink childrenCUSTOM page or a resource-backed template page (POST /projects/:projectId/pages)POST /projects lifecycle ops)Every endpoint except openapi.json needs a bearer token:
Authorization: Bearer <WEAVERSE_API_KEY>WEAVERSE_API_KEY); never hardcode it, never pass it as a ?apiKey= query param outside local testing — query params leak into server/CDN logs.The bearerAuth scheme accepts a shop API key, a scoped API key, a delegated MCP OAuth token, or (translation-unit routes only) a project-bound translation agent token. Scopes:
| Scope | Grants |
|---|---|
content:read | Read projects, pages, theme settings, versions, drafts, translation units |
content:write | Save drafts and non-destructive edits (items, metadata, SEO, assignments, languages, glossary). Cannot call the explicit publish/schedule/restore/delete endpoints — but its live edits (`PATCH .../pages/...`, page create) reach the storefront immediately |
content:publish | Publish drafts, schedule/reschedule/cancel publications, restore versions, publish global sections |
content:delete | Delete pages/global sections/languages/static keys, override reverts; required with project:manage for project deletion |
project:manage | Create/copy/delete projects, settings, variants, hierarchy, backups, previews, hostname policy |
translations:read / translations:write | Translation-unit routes only; never general content access |
403 INSUFFICIENT_SCOPE with nothing written.DELETE .../pages still works for keys minted before scopes existed). Treat such a key as destructive-capable and replace it with a scoped key.expectedUpdatedAt on page patches, expectedRevision on theme-settings — omitted → 428 PRECONDITION_REQUIRED. API keys may omit them, but sending them is still the safe default.content:read + translations:write for a translation agent) so a leaked or runaway script cannot delete or publish. Prefer that for automation.The spec accepting delegated MCP OAuth tokens is an auth fact only. Hosting an MCP server or listing a Claude/ChatGPT connector is a separate activation this skill pack does not perform.
Read before you edit. To change existing content you must target real item ids, so read the page first — you cannot patch blindly. New items are the one exception: they use a fresh id plus a type, and must fit the page tree (a children reference must point at an id already on the page or created in the same request).
GET /projects, match by name, keep its id.GET /projects/:projectId/languages. Keep the isDefault: true code (e.g. en-us). An empty data: [] is a valid state: the project has no configured languages, everything lives on the base locale — don't guess or invent a code; an explicit locale=en-us read reaches the base page through the fallback chain.GET /projects/:projectId/pages/:type/*handle?locale=<code>. Always pass `locale`. Resolution falls back requested locale → project default → en-default, so a missing locale can still succeed while returning a different locale's page — the edit then lands on the wrong page. The default weaverse format already returns every item with its `id` — that id is exactly what the patch needs (?meta=true only matters for portable-text reads). Keep the response's updatedAt; it is the optimistic-concurrency token for the patch.data (it shallow-merges; nested objects are replaced wholesale). Every entry needs both `id` and `data` — data is not optional, even for a topology-only relink; use "data": {}. To create a new item, give it a fresh id and supply its type. Include the same `locale` you read with, and the read's updatedAt as expectedUpdatedAt: {
"locale": "en-us",
"expectedUpdatedAt": "2026-10-21T12:00:00.000Z",
"items": [
{ "id": "itm1", "data": { "heading": "New heading" } },
{ "id": "itm-new", "type": "Hero", "data": { "heading": "Fresh section" }, "children": [{ "id": "itm1" }] }
]
}PATCH /projects/:projectId/pages/:type/*handle (POST accepted if your client/proxy can't send a PATCH body). Max 100 items per request — chunk larger edits. Add "exact": true to refuse locale/handle fallback and write only the assignment whose stored (type, handle, locale) is exactly the one addressed (delegated writes are always exact).{ object: "page_update", requested, updated, created, notFound, failed, updatedIds, createdIds, notFoundIds, failedIds }. Non-empty notFoundIds = ids not on the resolved page (wrong page/locale, stale ids, or a new id sent without type) — re-read with the right locale, don't retry blind. failedIds = items whose outcome is unconfirmed after a mid-request stale detection — re-read and reapply those; don't assume they applied.Stale refusal: if the page changed since your read (e.g. a concurrent Studio save), the whole request is rejected 409 STALE_PAGE before anything is written. Re-read and reapply on top of the newer content — never retry the same patch. Theme settings behave the same with expectedRevision (409 STALE_PROJECT).
A successful live patch invalidates caches and is served through api.weaverse.io — the same path a Studio save takes. When the response is ambiguous (failedIds, cacheDelivery: "degraded" on publish/delete), re-read the resource to confirm the actual state before reporting success.
A page created with POST /pages (or an empty template) has only a root main item. To build the whole page from an import-style JSON:
main item id.{ "id": <rootId>, "data": {}, "children": [...] } (data required, even empty), plus every section/block as a new item with id, type, data, children.updated: 1, created: N, notFound: 0.Use fresh ids per page (don't reuse ids from another page or an earlier test render). Content sent this way skips Studio's import, so schema presets are not applied — every non-default value must be in data.
PATCH /projects/:projectId/theme-settings with { "theme": { ...changedKeys } } shallow-merges top-level theme keys (nested objects replaced wholesale — read them first), writes the project's own config only, and records a restorable ThemeVersion. Send the GET response's revision as expectedRevision. Theme keys are global: a product-card key restyles every product card on the site. The response returns the full merged theme and updatedKeys.
GET/POST /projects/:projectId/page-settings/:pageId — op=rename (name/prefix), op=rename-custom (name + handle, assignmentId), op=set-seo (seo object). SEO is writable here; it is not part of the page-content PATCH.
When an edit must be reviewed or gated instead of going live immediately, use drafts. Addressing is by `pageId` — no locale/handle fallback, so a draft can never land on another assignment.
GET /projects/:projectId/page-drafts/:pageId. Returns the current draft head when one exists, else live content, plus the revision tokens a save must replay: headVersionId, pageUpdatedAt, projectUpdatedAt. With a draft head, the page/project tokens are the revisions that draft was built on — a save is refused once live moved past them.PUT .../page-drafts/:pageId with the full item list (≤200 items, each id + optional type/data/children; malformed graphs are 400) plus the three expected* tokens exactly as GET returned them (null included verbatim). Refused 409 STALE_PAGE when the draft head, live page, or project moved — nothing saved, re-read. Never touches live.POST /publish-versions/:versionId with { "projectId", "pageId" } (optionally replaceScheduledVersionId to atomically replace an existing scheduled version). Same promotion and cache fan-out as the Studio publish; needs content:publish. A failed cache invalidation after a committed publish reports cacheDelivery: "degraded" — content changed, delivery queued.DELETE .../page-drafts/:pageId by exact CAS on the head.GET /projects/:projectId/global-sections lists sections with usages. POST .../global-sections creates one ({ itemId, name }). Per section: GET .../global-sections/:sectionId reads the draft head (null when none); POST .../global-sections/:sectionId runs op=save-draft / discard-draft / publish-draft / rename / delete. save-draft/discard-draft are content:write; publish-draft needs content:publish; delete needs content:delete + confirm="<sectionId>". save-draft takes the full item list and the expectedHeadVersionId CAS token.
GET /projects/:projectId/versions?pageId=<id> (that page's versions), ?kind=theme (theme versions), ?kind=retired-pages (retired page versions, recovery), default ?kind=scheduled (current schedule, page + theme).GET /versions/:versionId?type=page|theme&projectId=<id>.POST /versions/:versionId/schedule { projectId, type, publishAt } (+ replaceScheduledVersionId for page versions; rejected 400 for theme). The version goes live automatically at `publishAt` — no further call; treat scheduling as approving a live change. publishAt must be future; a page/project already having a scheduled version is 409, nothing changed.PATCH /versions/:versionId (reschedule) and DELETE /versions/:versionId (cancel) on scheduled publications only — a version no longer scheduled is 409/404.POST /versions/:versionId/publish-now { projectId, type }.POST /versions/:versionId/restore { projectId, type, confirm: "<versionId>" }. Restoring overwrites live content, so it needs both content:publish and content:delete plus the confirm string. Treat it as a merchant-approved rollback, not a routine edit. On 409 (concurrent claim, nothing changed) never replay: re-read the version and live target, and reconfirm with the merchant for the current state first.Two protocols; both write only Translation rows — base page content, items, and every other locale stay untouched, and the project default language is rejected as a target (it is the source others translate from).
GET /projects/:projectId/page-translations/:pageId?locale=<code> returns every translatable key with its source, current translation, and the targetGeneration a writeback must replay. POST the same URL with entries echoed verbatim except translation (≤250 per request). Each entry is compare-and-set on both the source it answers for and the targetGeneration it read — a delayed agent can never overwrite a merchant's newer source text or another writer's newer translation; refused entries come back in stale and were not written. translation: "" clears a key (kept distinct from never-translated). After writing, GET again to read back.GET /projects/:projectId/translation-units?targetLocale=<code> (kind=text|image, status, cursor paging) inventories every unit the stored project schema approves — components without a stored schema or fields of other input types are never listed or writable. POST /projects/:projectId/translations/validate runs every check with zero writes (dry run); POST .../translations/apply writes the batch atomically under the project lock. Any stale source/target refuses the whole batch (409 STALE_TRANSLATION) and writes nothing; an identical retry converges to unchanged. These routes also accept project-bound translation agent tokens (translations:read/translations:write only).POST /projects with an op (scope project:manage):
op | Body | Effect |
|---|---|---|
create | name?, projectId?, config.themeName? | Blank or theme-seeded project |
copy | sourceProjectId, name? | Background clone (202 + targetProjectId); source must be this shop's own or a public demo-store project; completion observed by listing the target's pages |
delete | projectId, confirm (= projectId) | Soft-delete with a pre-delete backup; also needs content:delete |
set-settings | projectId, expectedUpdatedAt, name?, defaultLocale? | Revision-fenced non-secret settings |
purge-cache | projectId | Studio "Clear cache" |
PATCH /projects/:projectId renames a project (name only, unknown fields 400). Ask the merchant before create/copy/delete — these change account-level state, not just one page's content.
read_publications/write_publications.PATCH /projects/:projectId accepts name only. (Hierarchy — variants, parent link/detach, page overrides — is writable through the dedicated POST /projects/:projectId/hierarchy route under project:manage, destructive ops confirm-gated; see references/endpoints.md.)Pages are addressed by page type + handle:
INDEX, PRODUCT, ALL_PRODUCTS, COLLECTION, COLLECTION_LIST, PAGE, BLOG,
ARTICLE, CART, CUSTOMER, NOT_FOUND, PASSWORD, SEARCH, CUSTOMINDEX, ALL_PRODUCTS, COLLECTION_LIST, CART, CUSTOMER, NOT_FOUND, PASSWORD, SEARCH) — one page per project, omit the handle.blogs/news).PRODUCT, COLLECTION, PAGE, BLOG, ARTICLE) — keep a shared default template at the empty handle, so a missing handle is rejected (it won't silently edit the template). Pass the real handle.…/pages/PRODUCT/<real-handle> succeeds (200) even when no per-resource override exists — the resolver falls back to the type's shared default template, and the returned id identifies that shared page. A PATCH then edits the template every resource of that type renders. Inspect the returned id before patching; create the per-resource override first (POST /pages clones the shared default) when the change is meant for one resource. Deliberate shared-template edits are fine — make them knowingly.Locale always matters. On reads/updates, always pass a real locale code (from List languages). In list-pages responses, a row's locale may be null for market-first projects (rows are keyed by market, not locale) — don't echo null back; pass a real code and let resolution map it to the market (e.g. locale=en-us resolves market us).
See references/endpoints.md for the full endpoint list, query params, and response shapes.
The Content API itself has no upload endpoint. To get media into a Weaverse item, upload it to Shopify first, then reference the returned CDN URL.
Upload goes through the Weaverse Shopify proxy, which accepts the same Weaverse token:
POST https://studio.weaverse.io/api/admin-graphql
Authorization: Bearer <WEAVERSE_API_KEY>
Content-Type: application/jsonThe body is a normal Shopify Admin GraphQL request ({ "query": "...", "variables": {...} }). Upload is the standard two-step Shopify flow:
stagedUploadsCreate → get a url + parameters (a presigned target) and a resourceUrl.url with the returned parameters (multipart POST, not through the proxy).fileCreate with originalSource: <resourceUrl> → Shopify ingests it and returns the permanent CDN file.image.url / sources and put that CDN URL into the Weaverse item data via the update workflow above.Reference implementation in the builder repo: app/backend/admin/file.server.ts (generateStagedUploadLinks → stagedUploadsCreate, then fileCreate). When in doubt, mirror its mutations and field selections.
Alternatively, when a connected Shopify MCP is available, its image-upload / graphql_mutation tools do the same job without the proxy. Use whichever is connected.The helper script's upload command runs all four steps and prints the CDN URL, id and size per file. Send a User-Agent header on proxy calls — requests without one are rejected with 403. The proxy is a normal Admin GraphQL endpoint, so other admin mutations the token allows (e.g. menuUpdate for navigation menus, fileDelete) also work through it.
scripts/weaverse_content_api.mjs wraps auth and the common calls. It reads WEAVERSE_API_KEY from the environment.
export WEAVERSE_API_KEY=...
node scripts/weaverse_content_api.mjs projects
node scripts/weaverse_content_api.mjs languages <projectId>
node scripts/weaverse_content_api.mjs theme <projectId>
node scripts/weaverse_content_api.mjs theme-update <projectId> <theme.json> # flat { "key": value }; the script wraps it in { theme } and replays the current revision as expectedRevision (409 STALE_PROJECT on a concurrent change)
node scripts/weaverse_content_api.mjs pages <projectId> [type]
node scripts/weaverse_content_api.mjs page <projectId> <type> [handle] [locale] # reads with ?locale
node scripts/weaverse_content_api.mjs create-page <projectId> <type> <handle> [name]
node scripts/weaverse_content_api.mjs update <projectId> <type> [handle] <patch.json> # omit handle for INDEX & other singletons
node scripts/weaverse_content_api.mjs delete <projectId> <type> <handle...>
node scripts/weaverse_content_api.mjs delete-ids <projectId> <pageId...> # projects without languages
node scripts/weaverse_content_api.mjs upload <file...> # → Shopify Files, prints CDN URLsUse it to inspect a project quickly and to apply patch files. For anything the script doesn't cover — drafts, publishing, versions, translations, lifecycle ops — call the REST endpoints directly (see references/endpoints.md) or read openapi.json.
PUT page-drafts, global-section save-draft) and pending-version PUT /versions/{id} stay off live by themselves (a replaced scheduled version still goes live at its publishAt). Live item PATCH and POST /pages under plain content:write change the storefront immediately; schedule goes live automatically at publishAt; publish-versions/publish-now/restore go live at once.project:manage, delete + content:delete + confirm) change account-level state. op=copy returns 202 and keeps running in the background; observe completion by listing the target's pages.POST /versions/:id/restore overwrites live content (needs content:publish + content:delete + confirm="<versionId>"). Merchant-approved rollback only.failedIds in a page_update means unconfirmed outcomes — re-read those items before assuming anything.428 PRECONDITION_REQUIRED. API keys may omit them; sending them anyway is the safe default.en-default, so the request can succeed on a different locale's page and the edit looks "missing" (distinct from PAGE_NOT_FOUND). Always pass a real code from List languages; use "exact": true on writes when fallback must be impossible.type is supplied; otherwise they land in notFoundIds. children ids must already be on the page or be created in the same request.locale); the weaverse read already includes every item id (?meta=true not needed).items entry must carry a data object; topology-only changes must still send "data": {} (item.data must be an object).en-us reaches the base page through the fallback chain.null for market-first projects. Don't send null; pass a real code and let resolution map it to the market.portable-text reads. On a weaverse-format read it changes nothing.Authorization: Bearer from an env var; mint a task-scoped key for automation instead of a full shop key.data and risk wiping nested values you didn't read.fileCreate step and use the returned Shopify CDN URL, not the staged/temporary resourceUrl."locale" cannot be an empty string). For a project with no languages, delete by pageIds (delete-ids).GET theme-settings before a theme-update (its revision doubles as the CAS token). ThemeVersions are restorable in Studio / via version restore.theme key instead of your settings. Check updatedKeys in the response.generating-weaverse-project-json — creates the import JSON that establishes the structure this API then updates. The item ids you patch here come from that JSON (or from a page read back with the right locale).cloning-websites-to-weaverse / figma-to-weaverse — produce the section plan that feeds the JSON generator.hydrogen-markets-localization — end-to-end markets/localization workflow this API's translation routes serve.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.