Render

Render requests & editing

Every render call is a render request with its own id (rnd_…), returned as renderId by POST /v1/templates/:id/render. A request holds its files in page order, the exact design it was rendered from (after your modifications) and where it came from. That stored design is what lets you re-render a request later, or let your users edit it in the embedded editor, without touching the master template.

ParamTypeNotes
renderIdrnd_…One render request — a single-page render, a whole carousel, or one PDF. Never shared: two identical calls get two ids.
fileIdren_…One output file inside a request (the id field on a template render response). GET /v1/renders/:renderId also accepts a file id and returns its request.
sourceTemplateIdtpl_… | nullThe template the chain started from — carried through rerenders and edits; null once that template is deleted. Renders outlive their template.
sourceRenderIdrnd_… | nullThe request it was derived from, for a rerender or an edit.
The design itself never leaves buildonce.app through the API: no endpoint on this page returns design JSON. Editing happens inside the embedded editor, which loads the design directly.
Render

GET /v1/renders

GET/v1/renders

Workspace-wide render history, newest first. Paginated and optionally filtered by format or template — suitable for polling (e.g. a 'new render' trigger).

ParamTypeNotes
pagenumberDefaults to 1.
limitnumberDefaults to 25, max 100.
format"png" | "jpeg" | "pdf" | "mp4" | "webm" | "gif"Restrict to one output format.
templateIdstringA template's public id (tpl_…).
curl "https://buildonce.app/v1/renders?format=png&limit=10" \
  -H "Authorization: Bearer fl_live_YOUR_API_KEY"
Response
{
  "items": [
    {
      "renderId": "rnd_8kq2m4x7ab1c",
      "sourceTemplateId": "tpl_123",
      "sourceRenderId": null,
      "format": "png",
      "createdAt": "2026-09-28T12:00:00.000Z",
      "credits": { "charged": 3, "balanceAfter": 1877 },
      "files": [ { "fileId": "ren_4hd9s0k2mq7z", "pageId": "s1", "url": "https://...", "width": 1080, "height": 1350 } ]
    }
    /* … */
  ],
  "total": 128,
  "page": 1,
  "limit": 25
}
Render

GET /v1/renders/:renderId

GET/v1/renders/:renderId

A render request's metadata and ordered files. Accepts a request id (rnd_…) or a file id (ren_…).

curl https://buildonce.app/v1/renders/rnd_8kq2m4x7ab1c \
  -H "Authorization: Bearer fl_live_YOUR_API_KEY"
Response
{
  "renderId": "rnd_8kq2m4x7ab1c",
  "sourceTemplateId": "tpl_123",
  "sourceRenderId": null,
  "format": "png",
  "createdAt": "2026-09-28T12:00:00.000Z",
  "credits": { "charged": 3, "balanceAfter": 1877 },
  "files": [
    {
      "fileId": "ren_4hd9s0k2mq7z",
      "pageId": "s1",
      "url": "https://.../renders/9de8...b5.png",
      "width": 1080,
      "height": 1350,
      "format": "png",
      "byteSize": 482113,
      "isEmpty": false
    }
    /* … one entry per page, in page order */
  ],
  "posts": [ /* GET only: the posts its publish block made */ ]
}

A video request (mp4, webm, gif) is always queued — poll this same endpoint until it finishes:

Response — still running
{
  "renderId": "rnd_8kq2m4x7ab1c",
  "status": "queued"
}
/* or "status": "processing" once drawing starts */
Response — failed
{
  "renderId": "rnd_8kq2m4x7ab1c",
  "status": "failed",
  "error": {
    "status": 422,
    "body": { "code": "VIDEO_TOO_LONG", "message": "..." }
  }
}

Once finished, this endpoint returns the same shape shown above, with status: "succeeded" added and a single file whose pageId is null (a video is always one file for the whole design, never one per page).

posts lists the social posts the request's publish block made (empty without one) — for a video, they appear once it is done.

Render

POST /v1/renders/:renderId/rerender

POST/v1/renders/:renderId/rerender

Render a request again from its stored design — not from the template's current draft — optionally with new modifications or another format. Returns a new request whose sourceRenderId is the one you passed.

ParamTypeNotes
modificationsRecord<string,string>Applied on top of the stored design. Page-scoped keys work here too — see Carousels below.
format"png" | "jpeg" | "pdf" | "mp4" | "webm" | "gif"Defaults to the source request's format. A video format answers 202 and is not stored — fps/scale/quality reset to their defaults unless passed again.
idempotencyKeystringA retry with the same key returns the same request and charges nothing.
publish{ accountIds, content, scheduledAt?, … }Post the new files to connected social accounts — the same block as the template render takes.
curl -X POST https://buildonce.app/v1/renders/rnd_8kq2m4x7ab1c/rerender \
  -H "Authorization: Bearer fl_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "modifications": { "page2@headline": "Updated for Q4" } }'
Editing

POST /v1/embed/:embedId/edit-sessions

POST/v1/embed/:embedId/edit-sessions

Open the embedded editor on a render request, for one of your users. Call it from your server with an API key; load the returned editorUrl in an iframe.

ParamTypeNotes
renderIdstring (required)The render request to edit (rnd_…).
originstring (required)Your page's origin, e.g. https://app.example.com. Must be on the embed's allowed domains when that list is set. The editor only ever posts messages to this origin.
allowPageChangesbooleanDefault true: the user may add, remove and reorder slides. false keeps the page structure fixed (switching between slides still works).
allowUnlockbooleanDefault false: layers locked in the design (a logo, a disclaimer) stay locked, in place and present. true lets the user unlock and change them.
ttlSecondsnumber60–7200. Defaults to 7200 (2 hours).
curl -X POST https://buildonce.app/v1/embed/emb_123/edit-sessions \
  -H "Authorization: Bearer fl_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "renderId": "rnd_8kq2m4x7ab1c",
    "origin": "https://app.example.com"
  }'
Response
{
  "sessionId": "res_2mf8a0c4pq9d",
  "renderId": "rnd_8kq2m4x7ab1c",
  "token": "fedit_…",
  "expiresAt": "2026-09-28T14:00:00.000Z",
  "editorUrl": "https://buildonce.app/e/emb_123?mode=render&session=fedit_…"
}
An editor link opens once. Reloading the iframe with the same link fails with EDIT_SESSION_ALREADY_OPENED — create a new session instead. Unsaved changes are kept as a draft of that render for 7 days and come back in the next session on it. There is one draft per render — per post, whoever edits it — so the last person to autosave wins until someone saves.
Editing

Render-mode editing routes

The editor loaded in the iframe calls these itself, authenticated with the Authorization: Bearer fedit_… token minted above — never with your API key. They exist for completeness; building your own editor UI against them instead of embedding BuildOnce's iframe is unsupported, since the design document itself is one of the request/response bodies and never otherwise leaves BuildOnce.

POST/v1/embed/:embedId/edit-session/open

Load the design to edit — the user's unsaved draft if one exists, otherwise the render request's stored snapshot. Single-use: calling it again on the same session fails with EDIT_SESSION_ALREADY_OPENED.

Response
{
  "renderId": "rnd_8kq2m4x7ab1c",
  "design": { /* the design document */ },
  "restoredDraft": false,
  "allowPageChanges": true,
  "allowUnlock": false,
  "hostOrigin": "https://app.example.com",
  "expiresAt": "2026-09-28T14:00:00.000Z",
  "brandTitle": "Acme Studio"
}
PUT/v1/embed/:embedId/edit-session/draft

Autosave the unsaved design. Renders nothing and charges nothing — one draft per render request, 7-day expiry, last write wins.

ParamTypeNotes
designobject (required)The editor's current design document.
Response
{ "saved": true }
POST/v1/embed/:embedId/edit-session/save

Save: render every page of the edited design as a new render request derived from the one the session opened, clear the draft, and advance the session to the new request. An unchanged design charges nothing and returns the current request.

ParamTypeNotes
designobject (required)The editor's current design document.
Response
{
  "sessionId": "res_2mf8a0c4pq9d",
  "renderId": "rnd_3bx8tsv9emk3",
  "sourceRenderId": "rnd_8kq2m4x7ab1c",
  "sourceTemplateId": "tpl_123",
  "unchanged": false,
  "files": [{ "pageId": "s1", "url": "https://…", "width": 1080, "height": 1350 }],
  "credits": { "charged": 4, "balanceAfter": 1873 }
}

This is the same payload the frameline:render:saved message and the render.edited webhook carry, documented below — the editor calls this route for you; you don't normally call it directly.

POST/v1/embed/:embedId/edit-session/assets

Upload an image or font opened from the editor's asset picker straight into the workspace's asset library.

ParamTypeNotes
filebinary (required)multipart/form-data, up to 15 MB.
kind"image" | "font" (required)
Response
{ "url": "https://.../asset.png", "fontFamily": null }
Every route above needs the session opened first (EDIT_SESSION_NOT_OPENED otherwise), and refuses any token not minted for this exact embed id (EMBED_MISMATCH) or expired (INVALID_EMBED_SESSION).
Editing

Editor messages

The editor talks to your page with postMessage, sent only to the origin you gave when creating the session — never to *. Check event.origin on your side too.

ParamTypeNotes
frameline:render:ready{ embedId }The editor page is running.
frameline:render:loaded{ renderId, pageCount, restoredDraft }The design is on the canvas. restoredDraft is true when an unsaved draft was restored.
frameline:render:change{ dirty }Sent when the unsaved-changes state flips — use it to warn before closing.
frameline:render:saved{ sessionId, renderId, sourceRenderId, sourceTemplateId, unchanged, files, credits }A save finished. files: [{ pageId, url, width, height }] in page order. unchanged: true means nothing was edited — the current renderId comes back and nothing is charged.
frameline:render:error{ code, message }Opening or saving failed — see Errors below.

The editor has its own Save button. To save from your own UI instead, post { type: "frameline:render:save" } to the iframe; it is accepted only from the parent page at the session's origin.

Your page
const EDITOR_ORIGIN = "https://buildonce.app";
const iframe = document.querySelector("#editor");
iframe.src = editorUrl; // from your server

window.addEventListener("message", (event) => {
  if (event.origin !== EDITOR_ORIGIN) return;
  const message = event.data;
  switch (message.type) {
    case "frameline:render:change":
      setUnsaved(message.dirty);
      break;
    case "frameline:render:saved":
      showImages(message.files.map((file) => file.url));
      rememberRender(message.renderId); // edit this one next time
      break;
    case "frameline:render:error":
      showError(message.message);
      break;
  }
});

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

render.edited webhook

Every save that changed something is also sent to the webhook endpoints subscribed to render.edited (Settings → Webhooks) — the reliable record even if the user closed the page before the message arrived. The data is the same object as the saved message, plus the embedId. Failed deliveries are retried.

POST to your webhook endpoint
BuildOnce-Event: render.edited
BuildOnce-Delivery-Id: whd_6tq2k9m4xa1b
BuildOnce-Signature: t=1790606243,v1=5f0c…e91a

{
  "id": "evt_8m2kq4x7tb1c",
  "type": "render.edited",
  "created_at": "2026-09-28T12:05:00.000Z",
  "workspace_id": "ws_123",
  "data": {
    "sessionId": "res_2mf8a0c4pq9d",
    "renderId": "rnd_3bx8tsv9emk3",
    "sourceRenderId": "rnd_8kq2m4x7ab1c",
    "sourceTemplateId": "tpl_123",
    "embedId": "emb_123",
    "files": [{ "pageId": "s1", "url": "https://…", "width": 1080, "height": 1350 }],
    "credits": { "charged": 4, "balanceAfter": 1873 }
  }
}

Verify every delivery the same way as any other webhook — see verifying a delivery for the signature header format and working Node/Python verification code.

Render

Credits

  • 1 credit per image, on every request — a carousel of 4 slides costs 4.
  • A PDF costs 1 credit per page.
  • A video (mp4, webm, gif) costs 1 credit per started second, at every size and quality — a design under 60 s plays in full; longer than that is rejected before rendering.
  • A request identical to an earlier one is still a new request and is still charged; its files are served from storage, not drawn again.
  • A retry with the same idempotencyKey returns the original request and costs 0.
  • An editor save with no changes costs 0 and returns the current request (unchanged: true). A save that changed one slide renders — and charges for — every slide, because it is a complete new request.
Render

Carousels

  • Files always match the design: same number, same order. A page with nothing on it renders as a blank image with isEmpty: true — it is never silently skipped.
  • Slides added, removed or reordered in the editor come back that way in the new request's files.
  • Set a value on one slide with a page-scoped key: page2@headline (by position, from 1) or s2@headline (by page id). A page id beats a position, which beats a plain headline. Works on template renders and rerenders.
modifications
{
  "headline": "Used on every slide without its own value",
  "page1@headline": "Slide one",
  "s3@headline": "The slide whose id is s3"
}
Render

Errors

ParamTypeNotes
404No render request with that id in your workspace — including another workspace's id.
400 INVALID_MODIFICATIONSA modification value is not a string.
403 EDIT_SESSION_FORBIDDENEdit sessions are created with an API key or dashboard session, not an embed token.
403 EMBED_DISABLEDThe embed is turned off.
400 INVALID_ORIGINorigin is not an http(s) origin.
403 DOMAIN_NOT_ALLOWEDorigin is not on the embed's allowed domains.
403 EDIT_SESSION_REQUIREDA render-mode route was called without a fedit_ token.
403 EMBED_MISMATCHThe fedit_ token was minted for a different embed id.
409 EDIT_SESSION_ALREADY_OPENEDThe editor link was already used — create a new session.
409 EDIT_SESSION_NOT_OPENEDdraft, save or assets was called before open.
401 INVALID_EMBED_SESSIONThe editor link has expired or is invalid.
400 PAGE_CHANGES_NOT_ALLOWEDA save changed the slides of a session with allowPageChanges: false.
400 LOCKED_LAYER_CHANGEDA save moved, removed or unlocked a locked layer in a session with allowUnlock: false.
400 INVALID_VIDEO_OPTIONSvideo.fps, video.scale or video.quality is out of range.
422 VIDEO_TOO_LONGThe design's total duration is over 60 s. Shorten page durations or render fewer pages.
503 VIDEO_ENCODER_UNAVAILABLEThe video encoder is unavailable. Nothing was charged — retry the request.
503 QUEUE_UNAVAILABLECould not read the render queue. Try again in a moment.
500 RENDER_FAILEDA queued render failed after retrying. Try again.

Was this page helpful?