Render

POST /v1/templates/:id/render

POST/v1/templates/:id/render

Rasterize a template's current draft to an image, PDF or video, optionally substituting variable values first. Every call is a new render request (renderId); images and PDFs cost 1 credit per image or per page, a video costs 1 credit per started second — only a retry with the same idempotencyKey is free.

ParamTypeNotes
format"png" | "jpeg" | "pdf" | "mp4" | "webm" | "gif"Defaults to "png". mp4/webm/gif render every page as one animated video — each element's enter/exit animation and each page's transition, exactly as the studio plays it.
video{ fps?, scale?, quality? }mp4/webm/gif only, ignored for images. See the video options below.
modificationsRecord<string,string>Variable name → replacement value, applied before render. Page-scoped keys (page2@headline, <pageId>@headline) set one slide.
animation{ pages?, layers? }Change timing and animation for this render only, so one template gives different cuts. See the animation options below.
pageIdstringWhich page to render. Defaults to the active page.
allPagesbooleanRender every page, in order. PDF → one multi-page file; png/jpeg → one file per page. A page with nothing on it renders blank (isEmpty: true) rather than being skipped. Ignores pageId. Video always renders every page regardless of this flag.
idempotencyKeystringReuse the same value on a retry: you get the original request back and are not charged again.
publish{ accountIds, content, scheduledAt?, … }Also post the rendered files to connected social accounts, now or at a set time. See "Render and post" below.

video options (mp4/webm/gif only):

ParamTypeNotes
video.fpsnumber1–60. Default 30, except gif, which defaults to and is capped at 15.
video.scale0.5 | 1 | 2Size as a multiple of the design's own. Default 1. A scale-up stops at 2160 px on the longest side; gif is never scaled up. Same price at every size.
video.quality"small" | "standard" | "high"mp4/webm encoder quality. Default "standard". gif ignores it.

animation options — the template is never changed, and a wrong field or value is a 400 INVALID_ANIMATION listing every problem, before anything is drawn or charged:

ParamTypeNotes
animation.pagesRecord<page, { duration?, transition? }>Keyed by page id or page1, page2… duration: seconds (0.5–60, null for automatic). transition: { kind: "fade" | "slide-left" | "slide-right" | "wipe" | "zoom", duration?: 0–3 } into the next page, or null for a cut. A video's length, and so its price, follows the new timing.
animation.layersRecord<layer, { … }>Keyed by variable name or layer id (page2@headline for one page). Fields: enterAnim, exitAnim, animDuration, animEasing, animReverse, showAt, hideAt, ambientAnim, ambientSpeed, ambientStrength, ambientDirection, textAnim, textAnimDuration, highlightAt, highlightDuration, popAt — the values the studio's Animate tab offers. Images show each layer at rest.
A video is always asynchronous: the call answers 202 with { "renderId": "rnd_…", "status": "queued" } at once, never the finished file. Poll GET /v1/renders/:renderId until status is succeeded or failed, or listen for the render.completed webhook. png, jpeg and pdf are unaffected — they still return the finished render in the same call.
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 '{
    "format": "png",
    "modifications": { "headline": "50% off today" }
  }'
Response
{
  "success": true,
  "data": {
    "id": "ren_789",
    "renderId": "rnd_456",
    "templateId": "tpl_123",
    "format": "png",
    "url": "https://.../ren_789.png",
    "byteSize": 482113,
    "width": 1080,
    "height": 1080,
    "createdAt": "...",
    "credits": { "charged": 1, "balanceAfter": 1879 },
    "isEmpty": false
  }
}

A video request instead:

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 '{
    "format": "mp4",
    "video": { "fps": 30, "scale": 1, "quality": "standard" }
  }'
Response — 202 Accepted
{
  "success": true,
  "data": {
    "renderId": "rnd_456",
    "status": "queued"
  }
}

With allPages: true and format of png or jpeg, data is { "renderId": "rnd_…", "pages": [ /* one of the above per page */ ] } instead of a single object — one request, one file per page.

Keep the renderId: it is what GET /v1/renders/:renderId, rerender and edit sessions take.

A retry with the same idempotencyKey returns the original response with credits.charged of 0. Without one, an identical call is a new request and is charged again — its files are served from storage, not drawn twice.
Render

Render and post

Add a publish block and the rendered files are posted to your connected social accounts in the same call: one post per account, its media the render's files (an all-pages render is a carousel). Find account ids with GET /v1/social/accounts; accounts are connected in the dashboard.

ParamTypeNotes
publish.accountIdsstring[] (required)Connected account ids (sac_…), 1–10.
publish.contentstring (required)The post text. Each platform's own limits apply.
publish.scheduledAtstringISO 8601 with Z or an offset (2026-10-25T09:00:00+05:00), in the future. Omit to publish at once. A video still rendering when the time comes is posted as soon as it is done.
publish.linkUrlstringA link to attach, where the platform takes one.
publish.title / description / tagsstring / string / string[]LinkedIn and YouTube only.
publish.platformOptionsobjectKeyed by platform: instagram.postType, youtube.privacy / categoryId, pinterest.boardId, x.replyToPostId.
Every account is checked before anything is drawn. A mistake — an unknown account, text too long for a platform, a time in the past, a format the platform can't take (Instagram wants JPEG, TikTok and YouTube want video) — is a 400 PUBLISH_INVALID listing each account's problems, and nothing is charged. An account that needs reconnecting, or X's monthly limit, does not stop the render: that account's result carries the error.
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 '{
    "format": "png",
    "modifications": { "headline": "New post is live!" },
    "publish": {
      "accountIds": ["sac_li1", "sac_x9"],
      "content": "Check out our latest design!",
      "scheduledAt": "2026-10-25T09:00:00+05:00"
    }
  }'
Response
{
  "success": true,
  "data": {
    "id": "ren_789",
    "renderId": "rnd_456",
    "url": "https://.../ren_789.png",
    "...": "the render, as above",
    "publish": {
      "results": [
        {
          "accountId": "sac_li1",
          "ok": true,
          "post": { "id": "spo_123", "status": "scheduled", "renderId": "rnd_456", "warnings": [] }
        },
        {
          "accountId": "sac_x9",
          "ok": false,
          "error": { "code": "X_POST_LIMIT_REACHED", "message": "..." }
        }
      ],
      "warnings": []
    }
  }
}

A video answers 202 before it is drawn, with "publish": { "status": "pending", "accountIds": [...] }. Its posts are made once the video is done; poll GET /v1/renders/:renderId — its posts lists them. A render that fails posts nothing. An account refused only after the video was done (disconnected meanwhile) is recorded in the activity log as social_post.render_publish_refused.

A render request is posted to each account at most once: a retry with the same idempotencyKey returns the same posts. Change or cancel a scheduled post with the social post routes.

Was this page helpful?