Guides

Let your users edit a rendered design

Your app renders a post or a carousel from a template, shows it to a user, and the user wants to change something — a word, a photo, the order of the slides. This guide opens that exact render in the embedded editor, lets the user edit it, and gets the new images back. The master template is never touched, and your app never handles design JSON.

  1. Render from a template and keep the renderId.
  2. When the user clicks Edit, create an edit session on your server.
  3. Show the returned editorUrl in an iframe.
  4. On save, take the new renderId and image URLs.
You need an API key (full access — no scope to pick) and an embed (dashboard → Embed) that is enabled, with your site on its allowed domains. Add a webhook endpoint for render.edited under Settings → Webhooks if you want a server-side record of every save.
Guides

1. Render and keep the renderId

Every render response carries a renderId (rnd_…). Store it next to the post in your database — it is the handle for editing, rerendering and looking the render up later.

curl -X POST https://buildonce.app/v1/templates/tpl_123/render \
  -H "Authorization: Bearer fl_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "allPages": true, "modifications": { "page1@headline": "Rates hold steady" } }'
Guides

2. Create an edit session

When the user clicks Edit, your server checks they may edit that post, then asks buildonce.app for an editor link for its render. Pass your page's origin: the editor will only talk to it.

app.post("/posts/:id/edit", requireUser, async (req, res) => {
  const post = await db.posts.find(req.params.id, { owner: req.user.id });
  const response = await fetch("https://buildonce.app/v1/embed/emb_123/edit-sessions", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.BUILDONCE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      renderId: post.renderId,
      origin: "https://app.example.com",
      allowPageChanges: true, // let them add, remove and reorder slides
      allowUnlock: false,     // keep the logo and disclaimer locked
    }),
  });
  const { data } = await response.json();
  res.json({ editorUrl: data.editorUrl, sessionId: data.sessionId });
});
Create the session only after checking that the signed-in user may edit that post —buildonce.app edits whatever render your server names. A link opens once and lasts up to 2 hours; create a fresh one each time the editor is opened.
Guides

3. Show the editor

Put the editorUrl in an iframe and listen for the editor's messages. It has its own Save button; you can also save from your own button.

Your page
<iframe id="editor" style="width:100%;height:760px;border:0"></iframe>
<button id="save">Save</button>

<script type="module">
  const EDITOR_ORIGIN = "https://buildonce.app";
  const iframe = document.getElementById("editor");

  const { editorUrl } = await fetch("/posts/42/edit", { method: "POST" }).then((r) => r.json());
  iframe.src = editorUrl;

  let unsaved = false;
  window.addEventListener("message", (event) => {
    if (event.origin !== EDITOR_ORIGIN) return;
    const message = event.data;
    if (message.type === "frameline:render:change") unsaved = message.dirty;
    if (message.type === "frameline:render:saved") {
      // message.renderId, message.files[i].url — in slide order
      updatePreview(message.files.map((file) => file.url));
    }
    if (message.type === "frameline:render:error") alert(message.message);
  });

  document.getElementById("save").onclick = () =>
    iframe.contentWindow.postMessage({ type: "frameline:render:save" }, EDITOR_ORIGIN);

  window.addEventListener("beforeunload", (event) => {
    if (unsaved) event.preventDefault();
  });
</script>

Unsaved work is autosaved as a draft of the post for 7 days. If the editor is closed and opened again on the same render — by the same person or a teammate — the draft comes back (restoredDraft: true on the loaded message).

Besides the four messages above, the editor also sends frameline:render:ready once the iframe has booted and frameline:render:loaded once the design is on the canvas (carrying renderId, pageCount and restoredDraft) — see the full message list in the API reference.

Guides

4. Store the result

Each save that changed something is a new render with a new renderId, pointing back at the one it was edited from (sourceRenderId). Store the new id: the next edit should start from it. The render.edited webhook carries the same data plus the sessionId, signed and retried, so your server can record the save even if the browser never reports back.

Webhook handler (Node, Express)
app.post("/webhooks/buildonce", express.raw({ type: "application/json" }), async (req, res) => {
  if (!verify(req.body.toString(), req.get("BuildOnce-Signature"), process.env.BUILDONCE_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const { type, data } = JSON.parse(req.body);
  if (type === "render.edited") {
    await db.posts.updateByRenderId(data.sourceRenderId, {
      renderId: data.renderId,
      images: data.files.map((file) => file.url),
    });
  }
  res.sendStatus(200);
});

verify is in the API reference.

Guides

Good to know

  • A save renders every slide and costs 1 credit per image; a save with no changes costs nothing. See Credits.
  • Slides the user adds, removes or reorders come back that way in files. Set allowPageChanges: false to keep a fixed slide count.
  • Images and fonts the user uploads in the editor go to your workspace's asset library.
  • Want new text without opening the editor? Use rerender with page-scoped modifications.
  • The editor needs the design to show it, so a determined user can read it from their own browser. What leaves through the API, messages and webhook is only ids, image URLs and credits.

Was this page helpful?