Update Pages
Add, duplicate, replace, edit, delete or reorder pages of a template without re-sending the whole design
Updated
/v1/studio/templates/{templateId}/update-pagescurl -X PATCH "https://api.orshot.com/v1/studio/templates/<TEMPLATE_ID>/update-pages" \
-H "Authorization: Bearer <ORSHOT_API_KEY>" \
-H "Content-Type: application/json" \
-d '{
"operations": [],
"allow_reindex": true,
"base_updated_at": "<BASE_UPDATED_AT>",
"embed_user_id": "<EMBED_USER_ID>"
}'const res = await fetch("https://api.orshot.com/v1/studio/templates/<TEMPLATE_ID>/update-pages", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <ORSHOT_API_KEY>",
},
body: JSON.stringify({
"operations": [],
"allow_reindex": true,
"base_updated_at": "<BASE_UPDATED_AT>",
"embed_user_id": "<EMBED_USER_ID>"
}),
});
const data = await res.json();import requests
response = requests.patch(
"https://api.orshot.com/v1/studio/templates/<TEMPLATE_ID>/update-pages",
headers={"Authorization": "Bearer <ORSHOT_API_KEY>"},
json={
"operations": [],
"allow_reindex": True,
"base_updated_at": "<BASE_UPDATED_AT>",
"embed_user_id": "<EMBED_USER_ID>"
},
)
data = response.json()$ch = curl_init("https://api.orshot.com/v1/studio/templates/<TEMPLATE_ID>/update-pages");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer <ORSHOT_API_KEY>",
"Content-Type: application/json",
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"operations" => [],
"allow_reindex" => true,
"base_updated_at" => "<BASE_UPDATED_AT>",
"embed_user_id" => "<EMBED_USER_ID>"
]));
$data = json_decode(curl_exec($ch), true);
curl_close($ch);require "net/http"
require "json"
uri = URI("https://api.orshot.com/v1/studio/templates/<TEMPLATE_ID>/update-pages")
req = Net::HTTP::Patch.new(uri)
req["Authorization"] = "Bearer <ORSHOT_API_KEY>"
req["Content-Type"] = "application/json"
req.body = {
"operations": [],
"allow_reindex": true,
"base_updated_at": "<BASE_UPDATED_AT>",
"embed_user_id": "<EMBED_USER_ID>"
}.to_json
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)Enterprise Only
This endpoint is available only to first party clients like the Orshot MCP server and workspaces on the Enterprise plan. Checkout the Enterprise Pricing to get access.
Change one or more pages of a template in a single request: add a page, duplicate one, replace one, change its settings (name, canvas, background, audio, subtitles, duration), delete it or move it, without re-sending the whole design to Update Template. Only pages whose content changed get a new thumbnail and animated preview; every other page keeps the one that already matches it.
Which update endpoint?#
| You want to change | Use |
|---|---|
| Name, tags, canvas size, or the entire design at once | Update Template |
| Pages: add, duplicate, replace, edit settings, delete, reorder | Update Pages (this page) |
| Layers on a page: add, replace, edit (incl. saved animation timing), delete, restack | Update Layers |
| Values of existing dynamic layers: text, images, colors, style, position | Update Template Modifications |
Endpoint#
https://api.orshot.com/v1/studio/templates/:templateId/update-pagesQuery Parameters#
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
includeThumbnails | Boolean | No | false | When true, the response waits for the new thumbnails of the changed pages. Otherwise they are generated in the background. |
includeVideoThumbnails | Boolean | No | false | When true and at most 2 changed pages are animated, the response waits for their animated previews. Otherwise they are generated in the background. |
Request Examples#
// Append a page (no existing page changes number)
await fetch("https://api.orshot.com/v1/studio/templates/12345/update-pages", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <ORSHOT_API_KEY>",
},
body: JSON.stringify({
base_updated_at: "2026-10-02T09:12:44.120Z", // from GET /v1/studio/templates/12345
operations: [
{
op: "add",
page_data: {
name: "Pricing",
canvas: { width: 1080, height: 1080, backgroundColor: "#0F172A" },
elements: [
{ type: "text", content: "Plans from $9", x: 80, y: 120, width: 920, height: 120, parameterizable: true, parameterId: "price_line" }
]
}
}
]
}),
});// Rename page 2 and change its background; canvas merges, so width/height stay
await fetch("https://api.orshot.com/v1/studio/templates/12345/update-pages", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <ORSHOT_API_KEY>",
},
body: JSON.stringify({
operations: [
// By page id (stays valid after pages are reordered). By number: { op: "update", page: 2, set: { ... } }
{ op: "update", page_id: "b2c3d4e5-6f78-9012-bcde-f23456789012", set: { name: "Offer", canvas: { backgroundColor: "#FFFFFF" } } }
]
}),
});// Copy the cover to the end, then move it to the front.
// Moving changes existing page numbers, so allow_reindex is required.
await fetch("https://api.orshot.com/v1/studio/templates/12345/update-pages", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer <ORSHOT_API_KEY>",
},
body: JSON.stringify({
allow_reindex: true,
operations: [
{ op: "duplicate", page: 1 }, // or page_id: "a1b2c3d4-..."
{ op: "move", page: 4, position: 1 } // the copy is page 4 at this point; its page_id is in the response
]
}),
});{
"success": true,
"changed": true,
"template": {
"id": 12345,
"name": "Spring Sale Carousel",
"updated_at": "2026-10-02T09:14:03.511Z",
"pages": [
{ "page": 1, "page_id": "f0e1d2c3-...", "name": "Cover" },
{ "page": 2, "page_id": "a1b2c3d4-...", "name": "Cover" },
{ "page": 3, "page_id": "b2c3d4e5-...", "name": "Offer" },
{ "page": 4, "page_id": "c3d4e5f6-...", "name": "Back" }
]
},
"results": [
{ "index": 0, "op": "duplicate", "status": "applied", "page_id": "f0e1d2c3-...", "source_page_id": "a1b2c3d4-...", "page": 1 },
{ "index": 1, "op": "move", "status": "applied", "page_id": "f0e1d2c3-...", "page": 1 }
],
"reindexed": [
{ "page_id": "a1b2c3d4-...", "from": 1, "to": 2 },
{ "page_id": "b2c3d4e5-...", "from": 2, "to": 3 },
{ "page_id": "c3d4e5f6-...", "from": 3, "to": 4 }
]
}{
"error": "These operations change the numbers of existing pages (page 1 → 2, page 2 → 3, page 3 → 4). Render calls that address pages by number (\"page2@headline\", includePages) would then hit different pages. Send \"allow_reindex\": true to confirm, or add/duplicate pages at the end instead. Nothing was saved.",
"code": "reindex_not_allowed",
"details": [
{ "index": null, "code": "reindex_not_allowed", "error": "These operations change the numbers of existing pages (…)" }
]
}Rate Limits#
This endpoint shares the Enterprise limit of 30 requests per minute per API key with the other template endpoints.
URL Parameters#
| Parameter | Type | Required | Description |
|---|---|---|---|
templateId | Integer | Yes | The ID of the template to update |
Request Body#
| Parameter | Type | Required | Description |
|---|---|---|---|
operations | Array | Yes | 1 to 50 page operations, applied in order. See Operations. |
allow_reindex | Boolean | No | Required (true) when the operations change the number of any existing page: deleting a page that isn't the last, inserting or duplicating somewhere other than the end, or moving. |
base_updated_at | String | No | The template's updated_at from your last read. If the template changed since, nothing is written and you get 409. Without it, operations apply to the latest version. |
embed_user_id | String | No | Assign the template to an embed user, same as Update Template Modifications. |
Operations#
Every operation has an op. Operations that act on an existing page name it with page_id (stable, from Get Studio Template) or page (1-based number). A page number means the number at that point in the request, after earlier operations; page_id never changes, so it is the safer target.
Page number or page id#
Anywhere an operation names a page, send either one:
| Field | Type | What it is |
|---|---|---|
page_id | String | The page's id. It never changes, so it keeps pointing at the same page after pages are added, deleted or reordered. Read it from Get Studio Template or from template.pages in any response of this endpoint. |
page | Integer | The page number, starting at 1. Simple for a one-off edit, but it changes when pages are added, deleted or moved. |
If you send both, they must point at the same page, otherwise the request is refused with page_target_mismatch.
op | Fields | What it does |
|---|---|---|
add | page_data (page object), position (optional page number, default after the last page) | Adds a new page. Missing page and layer ids are generated. |
duplicate | page_id or page, position (optional, default after the last page) | Copies a page with new page and layer ids. |
replace | page_id or page, page_data | Replaces the page's whole content. The page keeps its id. |
update | page_id or page, set (object) | Changes page settings: name, canvas (merged, so { "backgroundColor": "#000" } keeps the size), audioTracks, subtitle, videoDuration, motion. A null value removes the field. Layers can't be changed here; use Update Layers. |
delete | page_id or page | Deletes the page. A template always keeps at least one page. |
move | page_id or page, position | Moves the page to that page number. |
Page objects use the same shape as pages_data in Create Studio Template. Layers may use the flat x, y, width, height and src fields.
Page numbers and allow_reindex#
Render calls address pages by number: "page2@headline" modification keys, includePages, and the page field of Update Template Modifications. If page 2 becomes page 3, those calls silently hit a different page. So any operation list that changes the number of an existing page is refused unless you send "allow_reindex": true, and the response's reindexed array lists every page that moved. Adding or duplicating at the end, and deleting the last page, never renumber anything.
Response Fields#
| Field | Type | Description |
|---|---|---|
success | Boolean | true when the request was valid |
changed | Boolean | false when the operations changed nothing (for example setting a name to its current value). Nothing is written and the version does not move. |
template.id | Integer | Template ID |
template.name | String | Template name |
template.updated_at | String | The new version. Send it as base_updated_at on your next write. |
template.pages | Array | Every page after the operations: page (number), page_id, name |
template.thumbnails | Object | Only with ?includeThumbnails=true: the new thumbnails of the changed pages |
results | Array | One entry per operation: index, op, status (applied or unchanged), page (final number), page_id, and source_page_id for duplicate |
reindexed | Array | Only when page numbers changed: { page_id, from, to } per existing page that moved |
warnings | Array | Only when present: non-blocking notes, such as unknown motion keys |
Errors#
Every error explains what to change. A 400 means nothing was saved: operations are all-or-nothing.
| Status | code | When |
|---|---|---|
| 400 | operations_required, operations_empty, too_many_operations | operations is missing, empty, or longer than 50 |
| 400 | unknown_op, invalid_operation | An operation isn't an object with a known op |
| 400 | page_object_in_page | A page object was sent as page; send it as page_data |
| 400 | page_target_required, page_not_found, invalid_page, invalid_page_id, page_target_mismatch | The target page is missing, doesn't exist, or page and page_id disagree. The message lists the pages that exist. |
| 400 | invalid_page_object, duplicate_page_id, page_id_mismatch | The page object is malformed, its id is taken, or it doesn't match the page it replaces |
| 400 | set_required, field_not_settable, invalid_canvas | update has no set, tries to change elements, id or generated fields, or has an invalid canvas |
| 400 | position_required, invalid_position | move without a valid position |
| 400 | last_page, too_many_pages, template_too_large | Deleting the only page, going past 200 pages, or leaving a design larger than 10 MB |
| 400 | no_pages | The template has no pages yet; send the full design with Update Template first |
| 400 | reindex_not_allowed, invalid_allow_reindex | Existing pages would be renumbered without "allow_reindex": true |
| 400 | invalid_motion | The operations introduce a structural motion error |
| 403 | The API key, plan or trial access doesn't include this endpoint | |
| 404 | The template isn't in your workspace | |
| 409 | base_updated_at is stale: fetch the template again and redo the operations on that version | |
| 423 | An approval flow locks the template; see Approvals |
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 50+ integrations
- White-label embed for your own app
- 100 free credits a month, no credit card required