Render from Studio Template
Render any template you've designed in Orshot Studio by making a POST request — as an image, PDF, or video, at any size, and optionally publish it.
Updated
/v1/studio/rendercurl -X POST "https://api.orshot.com/v1/studio/render" \
-H "Authorization: Bearer <ORSHOT_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"templateId": 1,
"modifications": {},
"response": {},
"pdfOptions": {}
}'const res = await fetch("https://api.orshot.com/v1/studio/render", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <ORSHOT_API_KEY>",
},
body: JSON.stringify({
"templateId": 1,
"modifications": {},
"response": {},
"pdfOptions": {}
}),
});
const data = await res.json();import requests
response = requests.post(
"https://api.orshot.com/v1/studio/render",
headers={"Authorization": "Bearer <ORSHOT_API_KEY>"},
json={
"templateId": 1,
"modifications": {},
"response": {},
"pdfOptions": {}
},
)
data = response.json()$ch = curl_init("https://api.orshot.com/v1/studio/render");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer <ORSHOT_API_KEY>",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"templateId" => 1,
"modifications" => [],
"response" => [],
"pdfOptions" => []
]));
$data = json_decode(curl_exec($ch), true);
curl_close($ch);require "net/http"
require "json"
uri = URI("https://api.orshot.com/v1/studio/render")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer <ORSHOT_API_KEY>"
req["Content-Type"] = "application/json"
req.body = {
"templateId": 1,
"modifications": {},
"response": {},
"pdfOptions": {}
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)Templates you design in Orshot Studio are Studio Templates — custom layouts whose text, images, and styles you parameterize and then render on demand from the API.
POST https://api.orshot.com/v1/studio/renderQuick start#
The smallest request: a template ID, your dynamic values, and the output you want back.
const res = await fetch("https://api.orshot.com/v1/studio/render", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <ORSHOT_API_KEY>",
},
body: JSON.stringify({
templateId: <TEMPLATE_ID>,
modifications: {
title: "Custom Title",
imageUrl: "https://acme.com/cover.png",
},
response: { type: "url", format: "png" },
}),
});
const { data } = await res.json();
// data.content → the rendered image URLEverything else on this page is optional and layers on top of this: render at any size, as a PDF or video, or publish it to social.
Response#
data holds your render. Its shape depends on the template — expand the matching tab:
One page → data is an object. Read the render at data.content (a string).
{
"data": {
"content": "data:image/png;base64,iVBORw0.....", // URL or base64, per response.type
"format": "png",
"type": "base64",
"responseTime": 325.22
}
}With response.type: "binary" the response body is the file itself, not JSON.
Multiple pages → data is an array, one entry per page.
{
"data": [
{ "page": 1, "pageId": "a1b2c3d4-…", "content": "https://storage.orshot.com/…/Xk3v9Qp1aBc.png" },
{ "page": 2, "pageId": "b2c3d4e5-…", "content": "https://storage.orshot.com/…/Lm7Tz2Wq8Rd.png" }
],
"format": "png",
"type": "url",
"responseTime": 3166.01,
"totalPages": 3,
"renderedPages": 3
}File names are random, so treat each URL as opaque. Each entry has page (1-based number) and pageId (stable id). Prefer pageId when referencing a specific page. Page numbers shift when pages are reordered, while pageId is stable for the page's lifetime.
When flow text overflows onto extra pages, those pages carry flow metadata so you know where each came from.
{
"data": [
{ "page": 1, "pageId": "a1b2c3d4-…", "content": "https://…/Xk3v9Qp1aBc.png" },
{ "page": 2, "pageId": null, "content": "https://…/Lm7Tz2Wq8Rd.png", "sourcePageId": "a1b2c3d4-…", "sourcePageNumber": 1, "flowPage": 1 },
{ "page": 3, "pageId": null, "content": "https://…/Pq4Hs6Vn1Ke.png", "sourcePageId": "a1b2c3d4-…", "sourcePageNumber": 1, "flowPage": 2 }
],
"format": "png",
"type": "url",
"totalPages": 3,
"renderedPages": 3,
"flowTotalPages": 2
}| Field | Where | Meaning |
|---|---|---|
sourcePageId | overflow page | Id of the source template page the overflow came from |
sourcePageNumber | overflow page | Which template page the overflow came from |
flowPage | overflow page | Overflow index (1 = first overflow page, 2 = second, …) |
flowTotalPages | top level | Total overflow pages generated |
See Flowing Content for setup.
If part of a request was skipped, such as an unknown parameter ID or a video scale your plan does not support, the render still succeeds and the response has a warnings array of { "title", "details" } objects. A binary response carries the same list in the X-Orshot-Warnings header.
Add a publish object and the response also includes a publish array with
each account's status — see Publish to social.
Request body#
| Field | Type | Required | Description |
|---|---|---|---|
templateId | number | Yes | Your template's ID — shown on the template's page and in the playground. |
modifications | object | No | Your dynamic values, keyed by parameter ID. Multi-page templates prefix keys with the page, e.g. page2@title. You can also set style parameters to change layer styles at render time. |
response | object | No | What to render and how to return it (format, type, size, …). See below. |
pdfOptions | object | No | PDF-only output controls (document title, margins, DPI, color mode, outlined text, page range). See Render as a PDF. |
videoOptions | object | No | Video-only output controls (fps, trim, audio, subtitles, page transitions). See Render as a video. |
publish | object | No | Publish the render to connected social accounts. See Publish to social. |
connections | array | No | Send the render to connected storage, such as Dropbox, Google Drive or S3. |
webhook_url, metadata | string | No | Async mode only. See Start an Async Render. |
lang | string | No | Language of the template's text, as a language tag such as el, ja, zh-Hant or tr. Leave it out to use the template language set in the Studio, if any. See Set the text language. |
translate | string | object | No | Translate the template's text with AI before rendering, e.g. "el" or { "to": "es-MX", "context": "real estate listing" }. 1 AI credit per 100 words. See Translate. |
The response object#
| Field | Type | Description |
|---|---|---|
format | string | Output format: png, webp, jpg, avif, pdf, mp4, webm, mov, mkv, gif. Defaults to png. |
type | string | How content is returned: url, base64, or binary. Defaults to base64. Video and GIF formats need url. Multi-page templates support binary only for pdf. |
mode | string | sync (default) waits for the render; async returns a job immediately — built for video and long renders. See Start an Async Render. |
scale | number | Output scale multiplier. 1 = template size, 2 = double, etc. For PDFs, pdfOptions.dpi sets the scale instead (DPI divided by 72). Videos and GIFs can render at 2 on the Scale plan (a 1080×1350 template renders at 2160×2700) and render at 1 on every other plan. A value above 2 is not supported for videos. A long video at 2 also falls back to 1: at 1080×1920 and 30fps, the limit is about 15 seconds per page. Every fallback renders anyway and adds a warning to the response saying what it rendered at. |
size | string | object | Replace the render size: a preset slug (e.g. "instagram-story"), "WIDTHxHEIGHT" (e.g. "1200x630"), or { "width": 1200, "height": 630 }. See Smart Resize. |
width + height | number | Explicit pixel dimensions instead of size (both required together, 10–5000px). |
extraSizes | array | object | Add the same design at extra sizes in one call. See Render multiple sizes. |
includePages | number[] | Multi-page only — render just these pages, e.g. [1, 3]. |
quality | number | 1–100. Compression for jpg and webp (default 90) and avif (default 50). png ignores it. On video formats it is used when videoOptions.quality is not set. |
fileName | string | Custom output file name (no extension). url/binary types only. Carousel files are suffixed -page-1, -page-2, …; url outputs also get a -[HASH] suffix to stay unique. For PDFs, it also becomes the document's Title metadata unless pdfOptions.title is set. |
Set the text language#
Most text renders correctly without this, including Greek, Cyrillic, Arabic and CJK. Send lang when the language changes how text is drawn:
lang | What changes |
|---|---|
el | Uppercase Greek drops its accents (ΝΕΑ ΤΙΜΗ, not ΝΈΑ ΤΙΜΉ). Orshot already does this for uppercase Greek layers when lang is left out. |
tr, az | Uppercase i becomes İ |
ja, zh-Hans, zh-Hant, ko | Line breaks follow that language's rules, and shared Chinese characters use its glyph shapes where the font has them |
{
"templateId": <TEMPLATE_ID>,
"lang": "ja",
"modifications": { "headline": "直営店オープン" }
}An invalid tag is ignored. Renders with different lang values are cached separately.
Smart Resize#
Render a template at a different canvas size without redesigning it — design once, then generate platform-ready outputs for every channel from the same template and modifications. Want to set this up visually first? See Smart Resize in Studio.
The layout adapts deterministically (no AI) — the same template and size always produce the same output:
- Everything scales uniformly, so proportions and text wrapping are preserved.
- Backgrounds and full-bleed images stretch to cover the new canvas.
- Elements re-anchor to the edge/center they were designed against (a top-left logo stays top-left; a footer stays pinned to the bottom).
- Grouped elements (e.g. a heading + subheading) move together as one unit.
- A safety guard guarantees the adapted layout never introduces overlapping elements.
Render at a different size#
size replaces the render size; the response shape is unchanged.
response: {
type: "url",
format: "png",
size: "instagram-story", // preset slug, or "1080x1920", or use width + height
}Available presets — or pass any "WIDTHxHEIGHT" string, or width + height (10–5000px):
| Preset | Size (px) |
|---|---|
instagram-story | 1080×1920 |
instagram-post | 1080×1080 |
instagram-post-portrait | 1080×1350 |
instagram-post-landscape | 1080×566 |
facebook-post | 1200×630 |
facebook-story | 1080×1920 |
facebook-cover | 851×315 |
twitter-post | 1200×675 |
twitter-header | 1500×500 |
linkedin-post | 1200×627 |
linkedin-banner | 1584×396 |
youtube-thumbnail | 1280×720 |
youtube-short | 1080×1920 |
tiktok-video | 1080×1920 |
pinterest-pin | 1000×1500 |
whatsapp-status | 1080×1920 |
og-image | 1200×630 |
blog-header | 1200×630 |
email-header | 600×200 |
website-banner | 1920×600 |
presentation-16-9 | 1920×1080 |
a4-document | 2480×3508 |
us-letter | 2550×3300 |
business-card | 1050×600 |
zoom-background | 1920×1080 |
leaderboard-ad | 728×90 |
medium-rectangle-ad | 300×250 |
Aliases: x-post = twitter-post, x-header = twitter-header, instagram-square = instagram-post, instagram-reel = instagram-story.
response.scalestill applies on top of the adapted size —size: "1080x1920"withscale: 2outputs 2160×3840.- Best with moderate aspect-ratio changes; extreme changes (e.g. square →
leaderboard-ad) stay safe but elements may look small relative to the canvas.
Render multiple sizes in one call#
Where size replaces the output, extraSizes is purely additive: your normal response is unchanged, and each output gains an extraSizes array nested right beside it.
Pass an array of sizes:
response: { type: "url", format: "png", extraSizes: ["1080x1920", "1080x1080", "1200x630"] }…or a named object, when you want a label to route on:
response: { type: "url", format: "png", extraSizes: { story: "1080x1920", square: "1080x1080" } }An entry can also map pages to sizes, keyed by page number or pageId. Pages you leave out keep their size, and the entry's size label reads custom:
response: { type: "url", format: "png", extraSizes: [{ "1": "1080x1920", "2": "instagram-post" }] }The extra sizes nest beside your render — single-page on the data object, multi-page on each page (so a size is always self-describing about which page it belongs to):
{
"data": {
"content": "https://…", // your normal render (native or `size`)
"extraSizes": [
{ "size": "1080x1920", "width": 1080, "height": 1920, "content": "https://…" },
{ "size": "1080x1080", "width": 1080, "height": 1080, "content": "https://…" }
]
}
}{
"data": [
{ "page": 1, "content": "https://…", "extraSizes": [ { "size": "1080x1920", "width": 1080, "height": 1920, "content": "https://…" } ] },
{ "page": 2, "content": "https://…", "extraSizes": [ { "size": "1080x1920", "width": 1080, "height": 1920, "content": "https://…" } ] }
]
}A named size adds a name field to each entry, e.g. { "name": "story", "size": "story", … }.
Combine with size — size sets the base render, extraSizes add extra copies beside it:
// base rendered at 1200x630, plus a 1080x1920 copy
response: { type: "url", format: "png", size: "1200x630", extraSizes: ["1080x1920"] }Leave size off and data stays your original render — so extraSizes: ["1080x1920"] gives you the original and the story size.
Supported & billed. Image formats (png, jpg, webp, avif)
with response.type url or base64, and film
templates rendered as mp4. PDF and per-page video are not supported, and
extraSizes returns a 400 on templates with flowing (auto-expanding)
elements. Up to 50 extra outputs per call (pages × sizes). Each extra
output is billed like a page (2 renders = 1 credit). A size that fails
comes back with an error field instead of content — it doesn't break the
rest of the response and isn't billed.
Reproduce studio-approved sizes#
For pixel-perfect output, open your template in the studio and add sizes from the Smart Resize panel — each gets an instant adaptive preview, can be recomposed with AI, and is saved with the template. A render that requests a saved size reproduces that approved preview exactly, every time; the deterministic adaptation above is only used for sizes you haven't saved.
- Works with every response format and with multi-page templates (every page is adapted).
- Saved sizes apply whether the request comes from your own backend or an embed — your customers resize in the embed, you render the saved sizes via the API.
Render as a PDF#
Set response.format to pdf and add an optional pdfOptions object for document metadata and print controls (title, margins, DPI, color mode, outlined text, image compression, page range).
body: JSON.stringify({
templateId: <TEMPLATE_ID>,
modifications: { name: "Sarah Chen", course: "UX Design" },
response: { type: "url", format: "pdf" },
pdfOptions: { title: "UX Design Certificate", margin: "0px", dpi: 300, colorMode: "cmyk" },
}),pdfOptions.title sets the PDF's Title metadata (shown in browser tabs and PDF reader window titles); it falls back to response.fileName when omitted. For accessible PDFs, describe image layers with the .alt parameter, e.g. "hero_image.alt": "123 Main St exterior photo".
Photo-heavy pages can run to tens of MB because the renderer stores scaled or cropped images as raw pixels. Add "imageFormat": "jpeg" to pdfOptions to re-encode them, which typically cuts 80 to 90% of the file size.
See PDF Options for every field (title, margins incl. per-side overrides, DPI, RGB/CMYK, image compression, page ranges) and print recommendations.
Render as a video#
For templates with video elements, set response.format to mp4, webm, mov, mkv, or gif.
body: JSON.stringify({
templateId: <TEMPLATE_ID>,
modifications: {
promoVideo: "https://example.com/clip.mp4", // a video element's parameter ID
"promoVideo.trimStart": 0,
"promoVideo.trimEnd": 10,
},
response: { type: "url", format: "mp4" }, // or "webm", "mov", "mkv", "gif"
videoOptions: { fps: 30, muted: true, combinePages: true, pageTransition: "fade" },
}),- Per-element controls (custom URL,
.trimStart,.trimEnd,.muted,.loop) → see Video Elements. - Render-level
videoOptions(fps, quality, trim, audio overrides, subtitles + styling, multi-page combine & transitions) → see Video Options. - Page-level audio tracks and subtitles set in Studio are included automatically; see Audio & Subtitles.
- Video rendering needs a plan that includes video. Other plans get a
403. - Video and GIF formats need
response.type: "url".base64andbinaryreturn a400.
For multi-page video, set videoOptions.combinePages: true to get one
combined video instead of one file per page (supported for mp4, webm,
mov and mkv; GIF always returns per-page files). A template with Film mode on
returns one video by itself; see below.
Render a film#
A template with a film block (Studio's Film mode, or film sent on create) is one video: its pages play as scenes joined by each page's transitionIn, with the film's music and cross-page layers mixed in. Rendering it as mp4 returns a single object in data, like a single-page template:
body: JSON.stringify({
templateId: <TEMPLATE_ID>,
modifications: { "page1@headline": "Launch day", "page3@cta": "Try it free" },
response: { type: "url", format: "mp4", mode: "async" },
}),{
"data": {
"content": "https://storage.orshot.com/.../film.mp4",
"format": "mp4",
"type": "url",
"videoDuration": 20.9,
"responseTime": 48211.4
}
}- The film's length is the scenes added up minus the transitions (both pages play during a transition). It counts against your plan's maximum video length.
includePagesrenders the listed pages one by one, as normal page videos.webm,mov,mkvandgifalso render page by page; image and PDF formats are unchanged.videoOptions.muted,videoOptions.sound(false mutes layer sound effects),videoOptions.music(false leaves out library music) andvideoOptions.fpsapply.fpssets the frame rate, up to 30. Without it the film'soutput.fpsis used.combinePages,pageTransition,duration,audioSourceand the subtitle options do not apply to a film; captions are not burned into films yet, and the render says so inwarnings.- Smart Resize works on films:
sizerenders the whole film at another size andextraSizesadds up to 4 more sizes per call. Each size is a full film render and is billed as one. Per-page size maps are refused because a film has one canvas. - The film's schema, transitions and limits are on the Film page.
Video renders can take minutes — longer than many HTTP client timeouts. Add
mode: "async" to the response object to get a job back immediately and
collect the result by polling or webhook: see
Start an Async Render.
Publish to social#
Add a publish object to post the render straight to connected social accounts — no extra calls. The response gains a publish array with each account's status.
body: JSON.stringify({
templateId: <TEMPLATE_ID>,
modifications: { title: "New post is live!" },
response: { type: "url", format: "png" },
publish: {
accounts: [1, 2], // social account IDs from your workspace
content: "Check out our latest design!",
},
}),accounts is required. Without it the render still goes through, nothing is posted, and publish is an object instead: { "status": "skipped", "code": "no_accounts", "error": "...", "message": "...", "action": { ... } }. To draft or schedule a post before an account is connected, use POST /v1/social/publish with platforms.
See Publish from API for the full object — drafts (isDraft), scheduling (schedule.scheduledFor, timezone), per-account platformOptions, TikTok tiktokSettings, statuses, and error handling.
Rate limits#
This endpoint is rate limited based on your plan. Check the response headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per minute for your plan |
X-RateLimit-Remaining | Remaining requests in the current window |
X-RateLimit-Reset | Seconds until the current window resets |
Retry-After | Seconds to wait (only on 429 responses) |
The same values are also sent as RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.
Ready to automate?
Start rendering images, PDFs and videos from your templates in under 2 minutes. Free plan, no credit card.
Get your API key- Image, PDF and video generation via API
- Visual editor with AI and smart layouts
- Zapier, Make, MCP and 40+ integrations
- White-label embed for your own app
- 100 free credits a month, no credit card required