What You’ll Learn
Replace Backgrounds
Swap scene background images or videos with your own media
Update Text
Modify scene text and title content
Change Music
Replace background music with a different track
Render Updates
Render the final video with all your modifications
Before You Begin
Make sure you have:- A Pictory API key
- Node.js or Python installed on your machine
- A completed storyboard preview job (see Create Storyboard Preview)
npm install axios
pip install requests
Workflow Overview
The update workflow fits between creating a storyboard preview and rendering the final video:You can only update existing elements; no new elements can be added. Each element is identified by a unique
id from the preview response.Step-by-Step Guide
Step 1: Create a Storyboard Preview
First, create a storyboard preview to generate the elements you will modify:import axios from "axios";
const API_BASE_URL = "https://api.pictory.ai/pictoryapis";
const API_KEY = "YOUR_API_KEY";
async function createPreview() {
const response = await axios.post(
`${API_BASE_URL}/v2/video/storyboard`,
{
videoName: "my_editable_video",
voiceOver: {
enabled: true,
aiVoices: [{ speaker: "Brian", speed: 100 }]
},
backgroundMusic: {
enabled: true,
autoMusic: true,
volume: 0.1
},
scenes: [
{
story: "AI is transforming how educators and creators work. By automating tasks like content generation, visual design, and video editing, AI will save time and enhance consistency.",
createSceneOnEndOfSentence: true
},
{
story: "This allows creators to focus on higher-level strategies and ensures a cohesive brand presence. AI can analyze social media interactions to tailor content to individual learner needs.",
createSceneOnEndOfSentence: true
}
]
},
{
headers: {
"Content-Type": "application/json",
Authorization: API_KEY
}
}
);
return response.data.data.jobId;
}
import requests
API_BASE_URL = "https://api.pictory.ai/pictoryapis"
API_KEY = "YOUR_API_KEY"
def create_preview():
response = requests.post(
f"{API_BASE_URL}/v2/video/storyboard",
json={
"videoName": "my_editable_video",
"voiceOver": {
"enabled": True,
"aiVoices": [{"speaker": "Brian", "speed": 100}]
},
"backgroundMusic": {
"enabled": True,
"autoMusic": True,
"volume": 0.1
},
"scenes": [
{
"story": "AI is transforming how educators and creators work. By automating tasks like content generation, visual design, and video editing, AI will save time and enhance consistency.",
"createSceneOnEndOfSentence": True
},
{
"story": "This allows creators to focus on higher-level strategies and ensures a cohesive brand presence. AI can analyze social media interactions to tailor content to individual learner needs.",
"createSceneOnEndOfSentence": True
}
]
},
headers={
"Content-Type": "application/json",
"Authorization": API_KEY
}
)
return response.json()["data"]["jobId"]
Step 2: Poll for Completion and Review Elements
Wait for the preview job to complete and review therenderParams.elements array to identify elements you want to update:
async function getPreviewElements(jobId) {
while (true) {
const response = await axios.get(
`${API_BASE_URL}/v1/jobs/${jobId}`,
{ headers: { Authorization: API_KEY } }
);
const { status, renderParams } = response.data.data;
if (status === "completed") {
console.log(`Found ${renderParams.elements.length} elements:\n`);
// Log each element's id, type, and key info
renderParams.elements.forEach(el => {
if (el.elementType === "backgroundElement") {
console.log(` [Background] id: ${el.id}`);
console.log(` type: ${el.type}, startTime: ${el.startTime}, duration: ${el.duration}`);
console.log(` url: ${el.visualUrl}\n`);
} else if (el.elementType === "SceneText") {
console.log(` [Scene Text] id: ${el.id}`);
console.log(` text: ${el.text.substring(0, 60)}...`);
console.log(` startTime: ${el.startTime}, duration: ${el.duration}\n`);
} else if (el.elementType === "layerItem" && el.type === "text") {
console.log(` [Title Text] id: ${el.id}`);
console.log(` text: ${el.text}`);
console.log(` startTime: ${el.startTime}, duration: ${el.duration}\n`);
} else if (el.elementType === "audioElement") {
console.log(` [Audio] id: ${el.id}`);
console.log(` url: ${el.url.substring(0, 60)}...\n`);
}
});
return renderParams;
} else if (status === "failed") {
throw new Error("Preview generation failed");
}
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
const jobId = await createPreview();
const renderParams = await getPreviewElements(jobId);
import time
def get_preview_elements(job_id):
while True:
response = requests.get(
f"{API_BASE_URL}/v1/jobs/{job_id}",
headers={"Authorization": API_KEY}
)
data = response.json()["data"]
if data["status"] == "completed":
elements = data["renderParams"]["elements"]
print(f"Found {len(elements)} elements:\n")
for el in elements:
if el["elementType"] == "backgroundElement":
print(f" [Background] id: {el['id']}")
print(f" type: {el['type']}, startTime: {el['startTime']}, duration: {el['duration']}")
print(f" url: {el['visualUrl']}\n")
elif el["elementType"] == "SceneText":
print(f" [Scene Text] id: {el['id']}")
print(f" text: {el['text'][:60]}...")
print(f" startTime: {el['startTime']}, duration: {el['duration']}\n")
elif el["elementType"] == "layerItem" and el["type"] == "text":
print(f" [Title Text] id: {el['id']}")
print(f" text: {el['text']}")
print(f" startTime: {el['startTime']}, duration: {el['duration']}\n")
elif el["elementType"] == "audioElement":
print(f" [Audio] id: {el['id']}")
print(f" url: {el['url'][:60]}...\n")
return data["renderParams"]
elif data["status"] == "failed":
raise Exception("Preview generation failed")
time.sleep(3)
job_id = create_preview()
render_params = get_preview_elements(job_id)
Found 16 elements:
[Audio] id: voiceOver
url: https://audios-prod.pictorycontent.com/polly/production/p...
[Audio] id: bgMusic
url: https://tracks.melod.ie/track_versions/23517/MEL565_15_1...
[Background] id: backgroundElement_20260306230202544742ecd40c041449dac21bb737c799201
type: video, startTime: 0, duration: 7.6
url: https://media.gettyimages.com/id/2262113992/video/professor-lectures...
[Title Text] id: 2026030623020497002befca37e444fb880a281abadc0c756
text: AI's Impact on Educators and Creators
startTime: 0, duration: 7.6
[Background] id: backgroundElement_202603062302035443dd4953bf1294b4283d4b8ba0259fd1a
type: video, startTime: 7.6, duration: 11.1
url: https://media.gettyimages.com/id/2253469260/video/businesses-are-using...
[Scene Text] id: SceneText_202603062302035443dd4953bf1294b4283d4b8ba0259fd1a
text: By <strong>automating</strong> <strong>tasks</strong> like <strong>...
startTime: 7.6, duration: 11.1
...more elements
Step 3: Update Elements
Now update the elements you want to modify. You can update one or many elements in a single request.When updating background visuals,
url is used for final rendering and visualUrl is used for preview playback. visualUrl can be a lower-resolution video or even a thumbnail image for faster preview loading; set visualType to match (e.g., "image" for a thumbnail). type always reflects the media type of url. If you use the same URL for both, set type and visualType to the same value.Replace a Background Visual
async function updateBackgroundVisual(jobId, renderParams, sceneStartTime, newUrl, mediaType = "video") {
// Find the background element for the specified scene
const bgElement = renderParams.elements.find(
el => el.elementType === "backgroundElement" && el.startTime === sceneStartTime
);
if (!bgElement) {
throw new Error(`No background element found at startTime ${sceneStartTime}`);
}
const response = await axios.put(
`${API_BASE_URL}/v2/video/storyboard/${jobId}/elements`,
[
{
id: bgElement.id,
elementType: "backgroundElement",
type: mediaType,
startTime: bgElement.startTime,
duration: bgElement.duration,
url: newUrl,
visualUrl: newUrl,
visualType: mediaType
}
],
{
headers: {
"Content-Type": "application/json",
Authorization: API_KEY
}
}
);
console.log("Background updated:", response.status === 204 ? "Success" : "Failed");
}
// Replace scene 1 background (startTime: 0) with a custom video
await updateBackgroundVisual(
jobId, renderParams, 0,
"https://your-cdn.com/product-demo.mp4",
"video"
);
// Replace scene 2 background (startTime: 7.6) with an image
await updateBackgroundVisual(
jobId, renderParams, 7.6,
"https://your-cdn.com/team-photo.jpg",
"image"
);
def update_background_visual(job_id, render_params, scene_start_time, new_url, media_type="video"):
# Find the background element for the specified scene
bg_element = next(
(el for el in render_params["elements"]
if el["elementType"] == "backgroundElement" and el["startTime"] == scene_start_time),
None
)
if not bg_element:
raise Exception(f"No background element found at startTime {scene_start_time}")
response = requests.put(
f"{API_BASE_URL}/v2/video/storyboard/{job_id}/elements",
json=[
{
"id": bg_element["id"],
"elementType": "backgroundElement",
"type": media_type,
"startTime": bg_element["startTime"],
"duration": bg_element["duration"],
"url": new_url,
"visualUrl": new_url,
"visualType": media_type
}
],
headers={
"Content-Type": "application/json",
"Authorization": API_KEY
}
)
print(f"Background updated: {'Success' if response.status_code == 204 else 'Failed'}")
# Replace scene 1 background (startTime: 0) with a custom video
update_background_visual(
job_id, render_params, 0,
"https://your-cdn.com/product-demo.mp4",
"video"
)
# Replace scene 2 background (startTime: 7.6) with an image
update_background_visual(
job_id, render_params, 7.6,
"https://your-cdn.com/team-photo.jpg",
"image"
)
Update Scene Text
Scene text elements support<strong> tags for keyword highlighting. The highlighted words will be rendered using the element’s keywordColor.
When updating the
text property, you must set textLines to an empty array []. The textLines array contains pre-calculated line break positions and character coordinates for the original text. Keeping stale textLines after changing the text will cause incorrect line breaks and character positioning. Setting it to [] ensures the system recalculates the layout during preview and render.async function updateSceneText(jobId, renderParams, sceneStartTime, newText) {
const textElement = renderParams.elements.find(
el => el.elementType === "SceneText" && el.startTime === sceneStartTime
);
if (!textElement) {
throw new Error(`No scene text element found at startTime ${sceneStartTime}`);
}
const response = await axios.put(
`${API_BASE_URL}/v2/video/storyboard/${jobId}/elements`,
[
{
id: textElement.id,
elementType: "SceneText",
type: "text",
startTime: textElement.startTime,
duration: textElement.duration,
text: newText,
textLines: [] // Clear textLines so line breaks are recalculated for the new text
}
],
{
headers: {
"Content-Type": "application/json",
Authorization: API_KEY
}
}
);
console.log("Scene text updated:", response.status === 204 ? "Success" : "Failed");
}
// Update scene text with keyword highlighting
await updateSceneText(
jobId, renderParams, 7.6,
"By <strong>automating</strong> repetitive <strong>tasks</strong>, AI frees up time for <strong>creative work</strong> and <strong>strategic thinking</strong>."
);
def update_scene_text(job_id, render_params, scene_start_time, new_text):
text_element = next(
(el for el in render_params["elements"]
if el["elementType"] == "SceneText" and el["startTime"] == scene_start_time),
None
)
if not text_element:
raise Exception(f"No scene text element found at startTime {scene_start_time}")
response = requests.put(
f"{API_BASE_URL}/v2/video/storyboard/{job_id}/elements",
json=[
{
"id": text_element["id"],
"elementType": "SceneText",
"type": "text",
"startTime": text_element["startTime"],
"duration": text_element["duration"],
"text": new_text,
"textLines": [] # Remove textLines so line breaks are recalculated for the new text
}
],
headers={
"Content-Type": "application/json",
"Authorization": API_KEY
}
)
print(f"Scene text updated: {'Success' if response.status_code == 204 else 'Failed'}")
# Update scene text with keyword highlighting
update_scene_text(
job_id, render_params, 7.6,
"By <strong>automating</strong> repetitive <strong>tasks</strong>, AI frees up time for <strong>creative work</strong> and <strong>strategic thinking</strong>."
)
Change Background Music
async function updateBackgroundMusic(jobId, newMusicUrl) {
const response = await axios.put(
`${API_BASE_URL}/v2/video/storyboard/${jobId}/elements`,
[
{
id: "bgMusic",
elementType: "audioElement",
type: "audio",
startTime: 0,
duration: 100000,
url: newMusicUrl
}
],
{
headers: {
"Content-Type": "application/json",
Authorization: API_KEY
}
}
);
console.log("Music updated:", response.status === 204 ? "Success" : "Failed");
}
// Replace background music with a new track
await updateBackgroundMusic(jobId, "https://your-cdn.com/upbeat-track.mp3");
def update_background_music(job_id, new_music_url):
response = requests.put(
f"{API_BASE_URL}/v2/video/storyboard/{job_id}/elements",
json=[
{
"id": "bgMusic",
"elementType": "audioElement",
"type": "audio",
"startTime": 0,
"duration": 100000,
"url": new_music_url
}
],
headers={
"Content-Type": "application/json",
"Authorization": API_KEY
}
)
print(f"Music updated: {'Success' if response.status_code == 204 else 'Failed'}")
# Replace background music with a new track
update_background_music(job_id, "https://your-cdn.com/upbeat-track.mp3")
Update Multiple Elements at Once
async function updateMultipleElements(jobId, renderParams) {
const elements = renderParams.elements;
// Find elements to update
const bg1 = elements.find(el => el.elementType === "backgroundElement" && el.startTime === 0);
const bg2 = elements.find(el => el.elementType === "backgroundElement" && el.startTime === 7.6);
const sceneText = elements.find(el => el.elementType === "SceneText" && el.startTime === 7.6);
const response = await axios.put(
`${API_BASE_URL}/v2/video/storyboard/${jobId}/elements`,
[
// Update scene 1 background
{
id: bg1.id,
elementType: "backgroundElement",
type: "video",
startTime: bg1.startTime,
duration: bg1.duration,
url: "https://your-cdn.com/intro-video.mp4",
visualUrl: "https://your-cdn.com/intro-video.mp4",
visualType: "video"
},
// Update scene 2 background
{
id: bg2.id,
elementType: "backgroundElement",
type: "image",
startTime: bg2.startTime,
duration: bg2.duration,
url: "https://your-cdn.com/product-shot.jpg",
visualUrl: "https://your-cdn.com/product-shot.jpg",
visualType: "image"
},
// Update scene text: set textLines to null so line breaks are recalculated
{
id: sceneText.id,
elementType: "SceneText",
type: "text",
startTime: sceneText.startTime,
duration: sceneText.duration,
text: "<strong>AI automation</strong> enables creators to focus on what matters most.",
textLines: []
},
// Update background music
{
id: "bgMusic",
elementType: "audioElement",
type: "audio",
startTime: 0,
duration: 100000,
url: "https://your-cdn.com/corporate-music.mp3"
}
],
{
headers: {
"Content-Type": "application/json",
Authorization: API_KEY
}
}
);
console.log("All elements updated:", response.status === 204 ? "Success" : "Failed");
}
await updateMultipleElements(jobId, renderParams);
def update_multiple_elements(job_id, render_params):
elements = render_params["elements"]
# Find elements to update
bg1 = next(el for el in elements if el["elementType"] == "backgroundElement" and el["startTime"] == 0)
bg2 = next(el for el in elements if el["elementType"] == "backgroundElement" and el["startTime"] == 7.6)
scene_text = next(el for el in elements if el["elementType"] == "SceneText" and el["startTime"] == 7.6)
response = requests.put(
f"{API_BASE_URL}/v2/video/storyboard/{job_id}/elements",
json=[
# Update scene 1 background
{
"id": bg1["id"],
"elementType": "backgroundElement",
"type": "video",
"startTime": bg1["startTime"],
"duration": bg1["duration"],
"url": "https://your-cdn.com/intro-video.mp4",
"visualUrl": "https://your-cdn.com/intro-video.mp4",
"visualType": "video"
},
# Update scene 2 background
{
"id": bg2["id"],
"elementType": "backgroundElement",
"type": "image",
"startTime": bg2["startTime"],
"duration": bg2["duration"],
"url": "https://your-cdn.com/product-shot.jpg",
"visualUrl": "https://your-cdn.com/product-shot.jpg",
"visualType": "image"
},
# Update scene text: set textLines to None so line breaks are recalculated
{
"id": scene_text["id"],
"elementType": "SceneText",
"type": "text",
"startTime": scene_text["startTime"],
"duration": scene_text["duration"],
"text": "<strong>AI automation</strong> enables creators to focus on what matters most.",
"textLines": []
},
# Update background music
{
"id": "bgMusic",
"elementType": "audioElement",
"type": "audio",
"startTime": 0,
"duration": 100000,
"url": "https://your-cdn.com/corporate-music.mp3"
}
],
headers={
"Content-Type": "application/json",
"Authorization": API_KEY
}
)
print(f"All elements updated: {'Success' if response.status_code == 204 else 'Failed'}")
update_multiple_elements(job_id, render_params)
Step 4: Render the Final Video
After updating elements, render the final video using the Render from Preview API:async function renderVideo(jobId) {
// Trigger render from the updated preview
const response = await axios.put(
`${API_BASE_URL}/v2/video/render/${jobId}`,
{},
{
headers: {
"Content-Type": "application/json",
Authorization: API_KEY
}
}
);
const renderJobId = response.data.data.jobId;
console.log("Render job started:", renderJobId);
// Poll for render completion
while (true) {
const statusResponse = await axios.get(
`${API_BASE_URL}/v1/jobs/${renderJobId}`,
{ headers: { Authorization: API_KEY } }
);
const { status, videoURL } = statusResponse.data.data;
if (status === "completed") {
console.log("Video ready:", videoURL);
return videoURL;
} else if (status === "failed") {
throw new Error("Render failed");
}
console.log(`Render status: ${status}`);
await new Promise(resolve => setTimeout(resolve, 10000));
}
}
const videoUrl = await renderVideo(jobId);
def render_video(job_id):
# Trigger render from the updated preview
response = requests.put(
f"{API_BASE_URL}/v2/video/render/{job_id}",
json={},
headers={
"Content-Type": "application/json",
"Authorization": API_KEY
}
)
render_job_id = response.json()["data"]["jobId"]
print(f"Render job started: {render_job_id}")
# Poll for render completion
while True:
status_response = requests.get(
f"{API_BASE_URL}/v1/jobs/{render_job_id}",
headers={"Authorization": API_KEY}
)
data = status_response.json()["data"]
if data["status"] == "completed":
print(f"Video ready: {data['videoURL']}")
return data["videoURL"]
elif data["status"] == "failed":
raise Exception("Render failed")
print(f"Render status: {data['status']}")
time.sleep(10)
video_url = render_video(job_id)
Element Types Quick Reference
The following element types are returned in therenderParams.elements array from a completed storyboard preview job:
| Element | elementType | type | id Pattern | Key Updatable Properties |
|---|---|---|---|---|
| Voice-Over | audioElement | audio | voiceOver | url |
| Background Music | audioElement | audio | bgMusic | url, fade |
| Scene Background | backgroundElement | video / image | backgroundElement_{uniqueId} | url, visualUrl, visualType, type, loop, mute, objectMode, colorOverlay |
| Scene Text | SceneText | text | SceneText_{uniqueId} | text (supports <strong> for keyword highlighting), fontFamily, fontSize, fontColor, keywordColor, textBackgroundColor, textAlign |
| Title/Layer Text | layerItem | text | {uniqueId} | text, fontFamily, fontSize, fontColor, textAlign |
| Layer Visual | layerItem | video / image | {uniqueId} | url, visualUrl, type, objectMode, opacity |
Element IDs are auto-generated unique strings. Always retrieve the actual IDs from the preview job response; do not hardcode or construct them manually.
For a complete reference of every attribute on each element type, including styling, animation, position presets, and audio segments, see the Element Attributes Reference in the API documentation.
Next Steps
Embed Preview Player
Preview updates in real-time using the embedded player
Search Media
Find stock videos and images to use as backgrounds
Search Music
Find background music tracks
API Reference
Full API reference for Update Storyboard Elements
