Skip to content

Bondlayer editor JSON — capture runbook

Status: active · Since: 2026-07-17 · Owner: import pipeline Consumes into: apps/bondlayer-worker/src/parser.py Target format: Matriz Site JSON v1 Companion: import pipeline (runbook) · xlsx CMS export Reverse-engineered from: archived …/tools/bondlayer-converter/docs/CLI-FETCHER-DESIGN.md

The import pipeline runbook assumes you already have editor.json on disk. This doc is how you get it — the one manual step the whole pipeline hangs off. Bondlayer has no public export API; you capture the authenticated editor-design response out of a logged-in browser session.


It’s Bondlayer’s project design payload — the single JSON the editor loads to open a project. parser.py reads exactly two top-level keys, project and design.

A raw capture (verified against the live endpoint and all three files in imports/ on 2026-07-26) is platform-nested:

{
"project": { "id": "sdjpmgddd8dlcouy", "name": "ACHRONIMA" },
"design": "{ …one big escaped JSON string… }" // ← usually stringified; json.loads it
}

…and that inner design object splits into per-platform and project-level halves:

{
"bond": "1.0.0",
"updated_at": 1758558905643,
// ── per platform ──────────────────────────────────────────────
"web": { "screens": {…}, "styles": {…}, "symbols": {…}, "repeaters": {…},
"settings": {…}, "content": {…}, "forms": {…}, "states": {…},
"variants": {…}, "fileVersion": … },
"mobile": { …same keys, usually near-empty… },
// ── project-level, shared by both platforms ───────────────────
"states": {…}, // global states — NOT under web/
"forms": {…},
"schemas": {…}, // collections; `web.schemas` is empty
"fonts": {…},
"imagePresets": {…},
"variants": {…}
}

web.states / web.forms are byte-identical duplicates of the project-level ones (verified); schemas exists only at project level.

Two shape quirks parser.py already absorbs — don’t be alarmed by either:

  • design arrives as a stringified JSON blob (one big escaped string) from the endpoint; parser.py json.loads-es it.
  • Flat vs nested. parser.py prefers design.web, else treats design itself as the container — so it handles both. Raw captures are nested (above); a flattened variant exists where screens/repeaters/states sit directly under design, alongside content and an exportedAt stamp. Flattened files are processed copies, not what the endpoint returns.

Reference captures in this repo:

File Shape Notes
imports/achronima_art/sdjpmgddd8dlcouy.editor.json raw / nested 2.1 MB · 37 screens · 60 repeaters · 11 states
imports/simaomartinez/nnp5fhzjcd.editor.json raw / nested 2.3 MB · 29 screens · 171 repeaters · 0 states
imports/12x12/nrkdmrhhrk.editor.json raw / nested 5.6 MB · 51 screens · 141 repeaters · 15 states (incl. author-made)
docs/Research/achronima-bondlayer-export.json flattened 4.6 MB, pretty-printed, has content + exportedAt

Diff your capture’s top-level shape against the matching row.


⚠️ Verify before trusting Method A. The endpoint and headers were originally reverse-engineered from an archived 2026-06-24 snapshot (archive/2026-06-24-doc-snapshot/tools/bondlayer-converter/docs/CLI-FETCHER-DESIGN.md). They were re-verified working on 2026-07-26, but the token location had already moved (localStorage → cookie — see the erratum below), so keep confirming against a live app.bondlayer.com session (Method B shows you exactly what to look for). If the private endpoint no longer matches, Method B is the robust fallback — it needs no knowledge of Bondlayer’s API at all. Method D (in-memory store) needs neither the API nor a token and is the best option for exploratory work.

GET https://backend.bondlayer.com/api/design/open/{siteId}
Headers:
Accept: application/json
x-access-token: {jwt}
  • siteId — the id in the editor URL app.bondlayer.com/project/{siteId}/design/web. Not always 16 chars — a project captured on 2026-07-26 used a 10-char id (nnp5fhzjcd), so the URL-regex in Method A can miss; take the path segment, not a length-matched run.
  • jwt — ⚠️ erratum: it is no longer in localStorage.accessToken (that key does not exist). As of 2026-07-26 it is the _bondUserToken cookie on app.bondlayer.com (document.cookie, alongside _bondUserId). Remember to decodeURIComponent it. Still short-lived — capture in one sitting.

The endpoint itself was confirmed working and returns the familiar { design: "<stringified JSON>", … } shape (~2.3 MB for a 29-screen project).

Method A — console snippet (auto-downloads)

Section titled “Method A — console snippet (auto-downloads)”

Open the project in the Bondlayer editor, then paste into the DevTools Console:

(async () => {
const raw = document.cookie.split(';').map(s => s.trim())
.find(s => s.startsWith('_bondUserToken='))?.slice('_bondUserToken='.length);
const token = raw && decodeURIComponent(raw);
if (!token) return console.error('No _bondUserToken cookie — log in first.');
// /project/{siteId}/design/web — take the path segment, ids vary in length.
const siteId = location.pathname.split('/')[2] || prompt('Bondlayer siteId:');
if (!siteId) return;
const res = await fetch(`https://backend.bondlayer.com/api/design/open/${siteId}`, {
headers: { Accept: 'application/json', 'x-access-token': token },
});
if (!res.ok) return console.error('Fetch failed', res.status, await res.text());
const json = await res.json();
const blob = new Blob([JSON.stringify(json, null, 2)], { type: 'application/json' });
const a = Object.assign(document.createElement('a'), {
href: URL.createObjectURL(blob),
download: `${siteId}.editor.json`,
});
a.click();
console.log('Saved', a.download, `(${(blob.size / 1e6).toFixed(1)} MB)`);
})();

The prompt() is there for the case where the editor URL isn’t the usual /project/{siteId}/design/web shape. A copy of this snippet that the user keeps to hand lives at imports/REFERENCE_user capture/Capture.txt.

Method B — Network-tab capture (no API knowledge required)

Section titled “Method B — Network-tab capture (no API knowledge required)”
  1. Log in at app.bondlayer.com and open the project in the editor.
  2. DevTools → Network → filter Fetch/XHR → tick Preserve log.
  3. Reload the editor.
  4. Find the large JSON response (hundreds of KB – several MB) whose body has a top-level design (containing screens/styles/symbols) and a project.
  5. Right-click it → Copy response (or Save response body) → save as <site>.editor.json.

Method C — curl (token + siteId pasted by hand)

Section titled “Method C — curl (token + siteId pasted by hand)”
Terminal window
curl -sS 'https://backend.bondlayer.com/api/design/open/<SITE_ID>' \
-H 'Accept: application/json' \
-H 'x-access-token: <JWT_FROM__bondUserToken_COOKIE>' \
-o '<SITE_ID>.editor.json'

Method D — read the editor’s live in-memory store (no API, no token)

Section titled “Method D — read the editor’s live in-memory store (no API, no token)”

Verified 2026-07-26. The editor keeps the whole project design in an Immutable.js store on window.bond.design.current. toJS() yields the same object as the endpoint’s design key, already parsed:

copy(JSON.stringify(bond.design.current.toJS(), null, 2)); // DevTools console

Top-level keys observed: screens, styles, symbols, repeaters, schemas, forms, states, settings, fonts, imagePresets, variants, content, fileVersion.

Two reasons to prefer this for research (not for bulk capture — it’s clipboard-bound):

  • No credential handling — nothing to copy out of cookies.
  • It reflects unsaved edits, so you can snapshot → change one control → snapshot → diff, and read off exactly which keys that control owns, without ever saving. This is how ../Research/BONDLAYER-REPEATER-QUERY-SEMANTICS.md was produced; see its §0 for the diff helper.

window.bond.editor.current.toJS() is the sibling UI-state store (selection, current screen, autosave, breakpoint, …) — useful for orienting, never part of a capture.

⚠️ Autosave. Projects can have bond.editor.current.toJS().autosave === true. Any UI change you make to explore will persist to the real project. Do exploratory work on a scratch screen and delete it afterwards — and note that Bondlayer leaks orphan repeaters, forms and private styles when a screen is deleted (see the query-semantics doc §6).


A partial or truncated capture will fail deep in the pipeline. Sanity-check first:

Terminal window
cd apps/bondlayer-worker/src
python3 parser.py ./<site>.editor.json

You should see a summary: screens, elements, styles, symbols, schemas, forms, etc. If it errors or reports 0 screens, the capture is incomplete — re-grab the full response body.


The raw editor JSON is not directly ingestible by the Import Wizard — the generic json adapter would flatten the whole site into one content item (see the warning in packages/import/src/adapters/site-json.ts). First convert it to a Matriz Site JSON v1 envelope with the offline CLI:

Terminal window
cd apps/bondlayer-worker/src
python3 import_bondlayer.py --editor-json ./<site>.editor.json \
--default-language pt --output base.json

base.json carries schemaVersion: "matriz-site-json/v1". Ingest it either way:

  • Import Wizard — drop base.json in; the site-json adapter claims it by schema version (probe 0.99) and runs provision → ingest verbatim.
  • Direct API — POST /api/import/site/:siteId with the envelope as the body, or let the CLI push live with --api-url <workers-host> --api-token <jwt>.

Full sequence, merge with CMS records, and the two-arm architecture: import pipeline runbook.


Per the archived data-locations table, the editor JSON is design-only. These live elsewhere and need a separate capture:

Data Where it actually lives How to get it
Design / styles / screens editor JSON this doc
Triggers & states editor JSON (element.attrs.triggers) this doc
CMS content records Bondlayer CMS export xlsx export → bondlayer-converter, merged into base.json
Custom code / custom CSS published HTML scrape <script src="*customcode*"> / <style> from the live site

Erratum (2026-07-25): the custom-code row above is wrong — site-wide custom CSS is in the editor JSON at design.settings.project.settings.webHeadStyles, and site-wide custom JS at the sibling webBodyScripts (both verified against the Achronima export; see ../Research/BONDLAYER-EXPORT-JSON-DEEP-DIVE.md §2/§6). A published-HTML scrape is only needed for anything loaded via external <script src="*customcode*"> files.

So a full-fidelity migration = editor JSON + xlsx CMS exports (+ optional published-HTML scrape for custom code). The editor JSON alone gives you a styled, structured, but content-empty site.


  • Third-party surface, archived provenance. Everything in §2 depends on Bondlayer’s private app internals as captured on 2026-06-24. Treat Method A/C as a convenience and Method B as the source of truth.
  • Tooling location (corrected 2026-07-26; deployment updated 2026-09-02). The Python importer is live and maintained at apps/bondlayer-worker/ — it ships as the matriz-import-runtime Cloudflare Container (edge/) since the D:importer-containers cutover; it previously ran as a Render Docker service, now retired (see plans/OWNER-OPS.md). Only the JS xlsx converter is frozen, at archive/2026-07-13-bondlayer-tooling/bondlayer-converter/; there is no bondlayer-import/ under archive/. Earlier revisions of this doc pointed every Python path at archive/ — those paths never resolved. The live end-to-end import into Neon was still gated (G13) at last review — see the import & framework review.
  • Token handling. The _bondUserToken cookie (formerly localStorage.accessToken) is a short-lived credential to a third-party account. Don’t commit it, don’t paste it into shared logs; it expires on its own. Method D avoids touching it entirely.